diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 363aa19f..d4e981df 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -97,7 +97,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = format_datetime( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -114,6 +116,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + row = self.get_true_data_row(row) self._undo.push( diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 29281d6f..788b1203 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -173,7 +173,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): }, data=self._data, undo=self._undo_stack, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Table.py b/src/View/BoundaryConditionsAdisTS/Edit/Table.py index eeba2689..e6c714d8 100644 --- a/src/View/BoundaryConditionsAdisTS/Edit/Table.py +++ b/src/View/BoundaryConditionsAdisTS/Edit/Table.py @@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -87,7 +87,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = format_datetime( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -104,6 +106,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + row = self.get_true_data_row(row) self._undo.push( diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Window.py b/src/View/BoundaryConditionsAdisTS/Edit/Window.py index 9ae31212..09670489 100644 --- a/src/View/BoundaryConditionsAdisTS/Edit/Window.py +++ b/src/View/BoundaryConditionsAdisTS/Edit/Window.py @@ -120,7 +120,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): }, data=self._data, undo=self._undo_stack, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/BoundaryConditionsTemperature/Edit/Table.py b/src/View/BoundaryConditionsTemperature/Edit/Table.py index 812cc5ec..196d68d7 100644 --- a/src/View/BoundaryConditionsTemperature/Edit/Table.py +++ b/src/View/BoundaryConditionsTemperature/Edit/Table.py @@ -24,7 +24,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -87,7 +87,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = format_datetime( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -104,6 +106,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + row = self.get_true_data_row(row) self._undo.push( diff --git a/src/View/BoundaryConditionsTemperature/Edit/Window.py b/src/View/BoundaryConditionsTemperature/Edit/Window.py index 5d875d3e..0c7306a4 100644 --- a/src/View/BoundaryConditionsTemperature/Edit/Window.py +++ b/src/View/BoundaryConditionsTemperature/Edit/Window.py @@ -117,7 +117,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): }, data=self._data, undo=self._undo_stack, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 4ada0795..41f20bbe 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -171,7 +171,7 @@ class ProfileWindow(PamhyrWindow): def profiles(self): """Return profiles from the geometry model owning this profile.""" - return self._profile.reach.reach.profiles + return self._profile.reach.profiles def navigate_profile(self, offset): profiles = self.profiles() diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 19aa0ff4..5399243e 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -818,6 +818,10 @@ class GeometryWindow(PamhyrWindow): # self.update_plot_rkc() def export_to_file(self): + export_interpolated = self._ask_export_options() + if export_interpolated is None: + return + settings = QSettings( QSettings.IniFormat, QSettings.UserScope, 'MyOrg' @@ -847,11 +851,9 @@ class GeometryWindow(PamhyrWindow): suffix = Path(filename).suffix if suffix != "": if suffix == ".st" or suffix == ".ST": - export_interpolated = self._ask_export_options() - if export_interpolated is not None: - self._export_to_file_st( - filename[:-3], export_interpolated - ) + self._export_to_file_st( + filename[:-3], export_interpolated + ) else: # Warning popup when the format is not exportable win = QtWidgets.QMessageBox() @@ -862,9 +864,7 @@ class GeometryWindow(PamhyrWindow): pass else: - export_interpolated = self._ask_export_options() - if export_interpolated is not None: - self._export_to_file_st(filename, export_interpolated) + self._export_to_file_st(filename, export_interpolated) def _ask_export_options(self): win = QtWidgets.QMessageBox(self) @@ -901,8 +901,9 @@ class GeometryWindow(PamhyrWindow): if is_interpolated and not export_interpolated: continue - self._export_to_file_st_profile(wfile, profile, pid) - pid += 1 + if profile.enabled: + self._export_to_file_st_profile(wfile, profile, pid) + pid += 1 def _export_to_file_st_profile(self, wfile, profile, pid): num = f"{pid:>6}" diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index 2c1e87ce..289b0265 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -23,7 +23,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -76,7 +76,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = format_datetime( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -93,6 +95,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + self._undo.push( SetDataCommand( self._data, row, column, value diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 6b01d249..62329aef 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -109,7 +109,8 @@ class EditLateralContributionWindow(PamhyrWindow): data=self._data, undo=self._undo_stack, trad=self._trad, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/LateralContributionsAdisTS/Edit/Table.py b/src/View/LateralContributionsAdisTS/Edit/Table.py index 5975c875..eacd21b1 100644 --- a/src/View/LateralContributionsAdisTS/Edit/Table.py +++ b/src/View/LateralContributionsAdisTS/Edit/Table.py @@ -24,7 +24,7 @@ from tools import ( trace, timer, timestamp_to_old_pamhyr_date, old_pamhyr_date_to_timestamp, - timestamp_to_old_pamhyr_date_adists + timestamp_to_old_pamhyr_date_adists, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -90,7 +90,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date_adists(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = format_datetime( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -107,6 +109,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + row = self.get_true_data_row(row) self._undo.push( diff --git a/src/View/LateralContributionsAdisTS/Edit/Window.py b/src/View/LateralContributionsAdisTS/Edit/Window.py index 174cc644..f828dc8c 100644 --- a/src/View/LateralContributionsAdisTS/Edit/Window.py +++ b/src/View/LateralContributionsAdisTS/Edit/Window.py @@ -124,7 +124,8 @@ class EditLateralContributionAdisTSWindow(PamhyrWindow): data=self._data, undo=self._undo_stack, trad=self._trad, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index dc8920fb..a3196f97 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -19,7 +19,7 @@ import logging import traceback -from tools import trace, timer +from tools import trace, timer, parse_datetime from Model.Except import NotImplementedMethodeError @@ -34,8 +34,11 @@ from PyQt5.QtWidgets import ( QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, QComboBox, QStyledItemDelegate, QHeaderView, + QMessageBox, ) +_translate = QCoreApplication.translate + logger = logging.getLogger() @@ -59,6 +62,27 @@ class PamhyrTextDelegate(QStyledItemDelegate): class PamhyrTableModel(QAbstractTableModel): + def date_to_elapsed_seconds(self, value): + """Convert an entered date to seconds since the study start date.""" + try: + entered_date = parse_datetime(value) + except ValueError as error: + QMessageBox.warning( + self._table_view, + _translate("PamhyrTableModel", "Invalid date/time"), + _translate( + "PamhyrTableModel", + "The entered date/time is invalid.\n" + "Expected formats:\n" + "YYYY-MM-DD [HH:MM[:SS]],\n" + "DD-MM-YYYY [HH:MM[:SS]] or\n" + "DD/MM/YYYY [HH:MM[:SS]]." + ) + ) + raise error + + return (entered_date - self._start_date).total_seconds() + def _setup_delegates(self): if self._table_view is None: return @@ -85,7 +109,8 @@ class PamhyrTableModel(QAbstractTableModel): undo=None, opt_data=None, options=["rows_selection"], - parent=None): + parent=None, + start_date=None): super(PamhyrTableModel, self).__init__() self._table_view = table_view @@ -102,6 +127,7 @@ class PamhyrTableModel(QAbstractTableModel): self._options = options self._undo = undo self._lst = [] + self._start_date = start_date self._setup_delegates() self._setup_lst() diff --git a/src/View/WeatherParameters/Edit/Table.py b/src/View/WeatherParameters/Edit/Table.py index a77b0e2d..2a9c8754 100644 --- a/src/View/WeatherParameters/Edit/Table.py +++ b/src/View/WeatherParameters/Edit/Table.py @@ -23,7 +23,7 @@ from datetime import date, time, datetime, timedelta from tools import ( trace, timer, timestamp_to_old_pamhyr_date, - old_pamhyr_date_to_timestamp + old_pamhyr_date_to_timestamp, format_datetime ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -77,7 +77,9 @@ class TableModel(PamhyrTableModel): if self._opt_data == "time": value = timestamp_to_old_pamhyr_date(int(v)) else: - value = str(datetime.fromtimestamp(v)) + value = format_datetime( + self._start_date + timedelta(seconds=float(v)) + ) else: value = f"{v}" @@ -94,6 +96,10 @@ class TableModel(PamhyrTableModel): column = index.column() try: + if (self._data.header[column] == "time" + and self._opt_data == "date"): + value = self.date_to_elapsed_seconds(value) + self._undo.push( SetDataCommand( self._data, row, column, value diff --git a/src/View/WeatherParameters/Edit/Window.py b/src/View/WeatherParameters/Edit/Window.py index 2e2bb878..d20beaab 100644 --- a/src/View/WeatherParameters/Edit/Window.py +++ b/src/View/WeatherParameters/Edit/Window.py @@ -121,7 +121,8 @@ class EditWeatherParametersWindow(PamhyrWindow): data=self._data, undo=self._undo_stack, trad=self._trad, - opt_data=self._study.time_system + opt_data=self._study.time_system, + start_date=self._study.date ) table.setModel(self._table) diff --git a/src/tools.py b/src/tools.py index 35f806fd..6422f808 100644 --- a/src/tools.py +++ b/src/tools.py @@ -22,7 +22,7 @@ import logging import sqlite3 import traceback -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from colorama import Fore @@ -239,6 +239,57 @@ def timestamp(dt: datetime): return dt.timestamp() +SUPPORTED_DATETIME_FORMATS = ( + "%d-%m-%Y %H:%M:%S.%f", + "%d-%m-%Y %H:%M:%S", + "%d-%m-%Y %H:%M", + "%d-%m-%Y", + "%d/%m/%Y %H:%M:%S.%f", + "%d/%m/%Y %H:%M:%S", + "%d/%m/%Y %H:%M", + "%d/%m/%Y", + "%d-%m-%y %H:%M:%S", + "%d/%m/%y %H:%M:%S", +) + + +def parse_datetime(value): + """Parse a supported date representation into a naive UTC datetime.""" + if isinstance(value, datetime): + parsed = value + else: + text = str(value).strip() + if text.endswith(("Z", "z")): + text = text[:-1] + "+00:00" + + try: + parsed = datetime.fromisoformat(text) + except ValueError as iso_error: + for date_format in SUPPORTED_DATETIME_FORMATS: + try: + parsed = datetime.strptime(text, date_format) + break + except ValueError: + continue + else: + raise ValueError( + f"Unsupported date format: {value!r}. Expected an ISO " + "date/time or one of: " + + ", ".join(SUPPORTED_DATETIME_FORMATS) + ) from iso_error + + # If the parsed datetime has timezone information, convert it to UTC and remove the timezone info + if parsed.tzinfo is not None: + parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None) + + return parsed + + +def format_datetime(value): + """Return a datetime using the canonical application format.""" + return parse_datetime(value).strftime("%Y-%m-%d %H:%M:%S") + + def date_iso_to_timestamp(date: str): if type(date) is str: return timestamp(datetime.fromisoformat(date))