From c170dbc46cfd26346a5424109d6a156e1ffcf5a3 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Tue, 18 Aug 2026 16:56:34 +0200 Subject: [PATCH 01/13] Mesh tab: modification of the mesh layout to better integrate it into the modeling process --- src/View/Geometry/Window.py | 4 ++++ src/View/MainWindow.py | 21 ++++++++++++++++++++- src/View/ui/MainWindow.ui | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 0827a094..5f3ad301 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -314,6 +314,10 @@ class GeometryWindow(PamhyrWindow): trad=self._trad, parent=self ) + dlg.adjustSize() + dialog_geometry = dlg.frameGeometry() + dialog_geometry.moveCenter(self.frameGeometry().center()) + dlg.move(dialog_geometry.topLeft()) if dlg.exec(): data = { "step": dlg.space_step, diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 1e5d3a88..05db840a 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -175,6 +175,7 @@ define_model_action = [ "action_menu_rep_additional_lines", "action_menu_run_adists", "action_menu_pollutants", "action_menu_d90", "action_menu_dif", "action_menu_edit_geotiff", + "action_menu_meshing", "action_menu_boundary_conditions_temperature", "action_menu_initial_conditions_temperature", "action_menu_weather_parameters", "action_menu_run_adistt", @@ -310,6 +311,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_scenarios": self.open_scenarios, "action_menu_edit_network": self.open_network, "action_menu_edit_geometry": self.open_geometry, + "action_menu_meshing": self.open_meshing, "action_menu_boundary_conditions": self.open_boundary_cond, "action_menu_boundary_conditions_sediment": self.open_boundary_cond_sed, @@ -351,6 +353,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # Current actions "action_toolBar_network": self.open_network, "action_toolBar_geometry": self.open_geometry, + "action_toolBar_meshing": self.open_meshing, "action_toolBar_boundary_cond": self.open_boundary_cond, "action_toolBar_lateral_contrib": self.open_lateral_contrib, "action_toolBar_frictions": self.open_frictions, @@ -1339,7 +1342,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): GeometryWindow, data=[self._study, self.conf, reach] ): - return + return self.get_sub_window( + GeometryWindow, + data=[self._study, self.conf, reach] + ) geometry = GeometryWindow( study=self._study, @@ -1348,8 +1354,21 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): parent=self ) geometry.show() + return geometry else: self.msg_select_reach() + return None + + def open_meshing(self): + """Open the current reach geometry and its meshing dialog.""" + geometry = self.open_geometry() + if geometry is None or self._study.is_read_only(): + return + + geometry.raise_() + geometry.activateWindow() + QApplication.processEvents() + geometry.edit_meshing() def open_boundary_cond_sed(self): self.open_boundary_cond(tab=1) diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 4d13ceb6..683f68b5 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -19,6 +19,7 @@ Serif + 75 true false @@ -64,6 +65,7 @@ Ubuntu + 50 false false @@ -87,6 +89,7 @@ Sans Serif + 50 false false @@ -126,6 +129,7 @@ &Geometry + @@ -257,6 +261,7 @@ Ubuntu + 50 false false @@ -275,6 +280,7 @@ Sans Serif + 50 false true @@ -325,6 +331,7 @@ Ubuntu + 50 false false @@ -346,6 +353,7 @@ + @@ -866,6 +874,30 @@ Open results temperature + + + + ressources/meshing.pngressources/meshing.png + + + Meshing + + + + + true + + + + ressources/meshing.pngressources/meshing.png + + + Meshing + + + Mesh geometry + + From 059d7203a9b297c6f2a4a503f02ae6300b31c938 Mon Sep 17 00:00:00 2001 From: JeaDylan <101608960+JeaDylan@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:42:17 +0200 Subject: [PATCH 02/13] Mesh: activate button in toolbar only when a study is selected --- src/View/MainWindow.py | 1 + src/View/ui/MainWindow.ui | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 05db840a..8e31bb35 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -158,6 +158,7 @@ other_model_action = [ define_model_action = [ # Toolbar "action_toolBar_network", "action_toolBar_geometry", + "action_toolBar_meshing", "action_toolBar_boundary_cond", "action_toolBar_lateral_contrib", "action_toolBar_frictions", "action_toolBar_initial_cond", # Menu diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 683f68b5..0b9f87de 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -884,6 +884,9 @@ + + false + true From ef5430ae2c2287a9f834d8040ce00052fd8fa12a Mon Sep 17 00:00:00 2001 From: JeaDylan <101608960+JeaDylan@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:01:55 +0200 Subject: [PATCH 03/13] Mesh: give choice to export only handwritten points in geometry, or all points with interpolated ones --- src/View/Geometry/Translate.py | 9 +++++++ src/View/Geometry/Window.py | 43 ++++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/View/Geometry/Translate.py b/src/View/Geometry/Translate.py index 10f93bd9..1b3702da 100644 --- a/src/View/Geometry/Translate.py +++ b/src/View/Geometry/Translate.py @@ -97,6 +97,15 @@ class GeometryTranslate(MainTranslate): self._dict["format_not_exportable"] = _translate( "Geometry", "The format of the file is not exportable." ) + self._dict["export_options"] = _translate( + "Geometry", "Export options" + ) + self._dict["export_options_text"] = _translate( + "Geometry", "Choose which cross-sections to export." + ) + self._dict["export_interpolated_profiles"] = _translate( + "Geometry", "Export interpolated cross-sections" + ) self._dict["update_rk_error"] = _translate( "Geometry", "RK update can't be executed." diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 5f3ad301..4beffb58 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -739,7 +739,11 @@ class GeometryWindow(PamhyrWindow): suffix = Path(filename).suffix if suffix != "": if suffix == ".st" or suffix == ".ST": - self._export_to_file_st(filename[:-3]) + export_interpolated = self._ask_export_options() + if export_interpolated is not None: + self._export_to_file_st( + filename[:-3], export_interpolated + ) else: # Warning popup when the format is not exportable win = QtWidgets.QMessageBox() @@ -750,16 +754,45 @@ class GeometryWindow(PamhyrWindow): pass else: - self._export_to_file_st(filename) + export_interpolated = self._ask_export_options() + if export_interpolated is not None: + self._export_to_file_st(filename, export_interpolated) - def _export_to_file_st(self, filename): + def _ask_export_options(self): + win = QtWidgets.QMessageBox(self) + win.setIcon(QtWidgets.QMessageBox.Question) + win.setWindowTitle(self._trad["export_options"]) + win.setText(self._trad["export_options_text"]) + win.setStandardButtons( + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel + ) + + export_interpolated = QCheckBox( + self._trad["export_interpolated_profiles"], win + ) + export_interpolated.setChecked(True) + win.setCheckBox(export_interpolated) + + if win.exec() != QtWidgets.QMessageBox.Ok: + return None + + return export_interpolated.isChecked() + + def _export_to_file_st(self, filename, export_interpolated=True): with open(filename+".st", "w+") as f: f.write("# Exported from Pamhyr2\n") - self._export_to_file_st_reach(f, self._reach) + self._export_to_file_st_reach( + f, self._reach, export_interpolated + ) - def _export_to_file_st_reach(self, wfile, reach): + def _export_to_file_st_reach(self, wfile, reach, + export_interpolated=True): pid = 0 for profile in reach.profiles: + is_interpolated = profile.name.lower().startswith("interpol") + if is_interpolated and not export_interpolated: + continue + self._export_to_file_st_profile(wfile, profile, pid) pid += 1 From 587e5412d52b96cae314afd7506dacea90ced08e Mon Sep 17 00:00:00 2001 From: JeaDylan <101608960+JeaDylan@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:17:49 +0200 Subject: [PATCH 04/13] Profiles: new functionnality, navigate between differents profiles (next and previous one) directly in the edit profile window --- src/View/Geometry/Profile/Window.py | 76 +++++++++++++++++++++++++++++ src/View/ui/GeometryCrossSection.ui | 27 ++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index bcafc75c..0d0a872a 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -128,6 +128,15 @@ class ProfileWindow(PamhyrWindow): self._tablemodel.blockSignals(False) def setup_connections(self): + navigation_actions = { + "action_previous_profile": self.previous_profile, + "action_next_profile": self.next_profile, + } + + for action in navigation_actions: + self.find(QAction, action)\ + .triggered.connect(navigation_actions[action]) + if self._study.is_read_only(): actions = {} else: @@ -152,6 +161,73 @@ class ProfileWindow(PamhyrWindow): .connect(self.update_points_selection) self._tablemodel.dataChanged.connect(self.update) + self.update_navigation_actions() + + def previous_profile(self): + self.navigate_profile(-1) + + def next_profile(self): + self.navigate_profile(1) + + def profiles(self): + """Return profiles from the geometry model owning this profile.""" + return self._profile.reach.reach.profiles + + def navigate_profile(self, offset): + profiles = self.profiles() + + try: + index = profiles.index(self._profile) + except ValueError: + self.update_navigation_actions() + return + + target_index = index + offset + if not 0 <= target_index < len(profiles): + self.update_navigation_actions() + return + + target = profiles[target_index] + if self._parent is not None and self._parent.sub_window_exists( + ProfileWindow, + data=[self._study, self._config, target] + ): + self.close() + return + + self.set_profile(target) + + def set_profile(self, profile): + self._profile = profile + self._hash_data[-1] = profile + self._undo_stack.clear() + + self._tablemodel.beginResetModel() + self._tablemodel._data = profile + self._tablemodel.endResetModel() + + self._plot.data = profile + self._plot.highlight = None + self._plot.draw() + + self._title = ( + self._trad[self._pamhyr_name] + + f" - {profile.name} {profile.rk}" + ) + self._set_title() + self.update_navigation_actions() + + def update_navigation_actions(self): + profiles = self.profiles() + try: + index = profiles.index(self._profile) + except ValueError: + index = -1 + + self.find(QAction, "action_previous_profile")\ + .setEnabled(index > 0) + self.find(QAction, "action_next_profile")\ + .setEnabled(0 <= index < len(profiles) - 1) def update_points_selection(self): rows = self.index_selected_rows() diff --git a/src/View/ui/GeometryCrossSection.ui b/src/View/ui/GeometryCrossSection.ui index dfc47bb6..8ecbcd8c 100644 --- a/src/View/ui/GeometryCrossSection.ui +++ b/src/View/ui/GeometryCrossSection.ui @@ -52,6 +52,9 @@ false + + + @@ -61,6 +64,30 @@ + + + + ressources/left.pngressources/left.png + + + Previous profile + + + Open the previous profile + + + + + + ressources/right.pngressources/right.png + + + Next profile + + + Open the next profile + + From 776d9ab6107446f3ca8b8b0f528add346581b4dd Mon Sep 17 00:00:00 2001 From: JeaDylan <101608960+JeaDylan@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:22:06 +0200 Subject: [PATCH 05/13] Profiles: keep security to not open the same profile twice --- src/View/Geometry/Window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 4beffb58..19e84363 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -287,13 +287,14 @@ class GeometryWindow(PamhyrWindow): if self.sub_window_exists( ProfileWindow, - data=[None, None, profile] + data=[self._study, self._config, profile] ): continue win = ProfileWindow( profile=profile, study=self._study, + config=self._config, parent=self, ) self._profile_window.append(win) From 5b778af7453847eddfad15071ba0ded540beb318 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 21 Aug 2026 11:30:26 +0200 Subject: [PATCH 06/13] Profiles: fix misconnexion between add/remove buttons and tab in actual profile window --- src/View/Geometry/Profile/Window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 0d0a872a..e59bc963 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -204,6 +204,8 @@ class ProfileWindow(PamhyrWindow): self._tablemodel.beginResetModel() self._tablemodel._data = profile + # update lst to display correct data and apply connexions to add/remove points + self._tablemodel._setup_lst() self._tablemodel.endResetModel() self._plot.data = profile From deb84ca331893495d5d9a6ae5d3573e692345306 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 21 Aug 2026 16:35:40 +0200 Subject: [PATCH 07/13] Profiles: can now disable some profiles, they still appears in the list, but they are marked as disabled --- src/Model/Geometry/ProfileXYZ.py | 44 +++++++++++++++++++++++----- src/Model/Geometry/Reach.py | 8 +++++ src/Model/Study.py | 2 +- src/View/Geometry/Table.py | 19 ++++++++++-- src/View/Geometry/Translate.py | 3 ++ src/View/Geometry/UndoCommand.py | 21 ++++++++++++++ src/View/Geometry/Window.py | 50 ++++++++++++++++++++++++++++++++ src/View/ui/GeometryReach.ui | 12 +++++++- 8 files changed, 146 insertions(+), 13 deletions(-) diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 905b2701..e2d52da0 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -58,7 +58,8 @@ class ProfileXYZ(Profile, SQLSubModel): num=0, nb_point: int = 0, code1: int = 0, code2: int = 0, - status=None, owner_scenario=-1): + status=None, owner_scenario=-1, + enabled=True): """ProfileXYZ constructor Args: @@ -90,6 +91,7 @@ class ProfileXYZ(Profile, SQLSubModel): self.time_l = 0.0 self._station = [] self.station_up_to_date = False + self._enabled = bool(enabled) self._get_water_limits_cache = {} self._get_water_limits_ac_cache = {} @@ -108,6 +110,7 @@ class ProfileXYZ(Profile, SQLSubModel): code1 INTEGER NOT NULL, code2 INTEGER NOT NULL, sl INTEGER, + enabled BOOLEAN NOT NULL DEFAULT TRUE, {Scenario.create_db_add_scenario()}, {Scenario.create_db_add_scenario_fk()}, FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id), @@ -152,6 +155,13 @@ class ProfileXYZ(Profile, SQLSubModel): "ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE" ) + if major == "0" and int(minor) <=2: + if int(release) < 9: + execute( + "ALTER TABLE geometry_profileXYZ " + + "ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE" + ) + return cls._update_submodel(execute, version, data) @classmethod @@ -237,7 +247,7 @@ class ProfileXYZ(Profile, SQLSubModel): table = execute( "SELECT pamhyr_id, ind, deleted, name, rk, num, " + - "code1, code2, sl, scenario " + + "code1, code2, sl, scenario, enabled " + "FROM geometry_profileXYZ " + f"WHERE reach = {reach.id} " + f"AND scenario = {scenario.id} " + @@ -258,6 +268,7 @@ class ProfileXYZ(Profile, SQLSubModel): code2 = next(it) sl = next(it) owner_scenario = next(it) + enabled = (next(it) == 1) profile = cls( id=pid, ind=ind, num=num, @@ -265,7 +276,8 @@ class ProfileXYZ(Profile, SQLSubModel): code1=code1, code2=code2, reach=reach, status=status, - owner_scenario=owner_scenario + owner_scenario=owner_scenario, + enabled=enabled ) if deleted: profile.set_as_deleted() @@ -318,12 +330,12 @@ class ProfileXYZ(Profile, SQLSubModel): execute( "INSERT OR REPLACE INTO " + "geometry_profileXYZ(pamhyr_id, deleted, ind, name, reach, " + - "rk, num, code1, code2, sl, scenario) " + + "rk, num, code1, code2, sl, scenario, enabled) " + "VALUES (" + f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " + f"{ind}, '{self._db_format(self._name)}', " + f"{self.reach.pamhyr_id}, {self.rk}, {self.num}, " + - f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}" + + f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}, {self._db_format(self._enabled)}" + ")" ) @@ -390,7 +402,8 @@ class ProfileXYZ(Profile, SQLSubModel): name=self.name, rk=self.rk, reach=self.reach, - status=self._status + status=self._status, + enabled=self.is_enabled ) if self.is_deleted(): new_p.set_as_deleted() @@ -410,7 +423,8 @@ class ProfileXYZ(Profile, SQLSubModel): name=self.name, rk=self.rk, reach=new_reach, - status=self._status + status=self._status, + enabled=self.is_enabled ) if self.is_deleted(): new_p.set_as_deleted() @@ -1131,7 +1145,8 @@ class ProfileXYZ(Profile, SQLSubModel): p = ProfileXYZ(name=self.name, rk=self.rk, reach=self.reach, - status=self._status) + status=self._status, + enabled=self.is_enabled) for i, k in enumerate(self.points): p.insert_point(i, k.copy()) @@ -1165,3 +1180,16 @@ class ProfileXYZ(Profile, SQLSubModel): break self._points = points self.modified() + + @property + def is_enabled(self): + return self._enabled + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, value): + self._enabled = bool(value) + self.modified() diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index d2fea850..04f1144e 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -157,6 +157,14 @@ class Reach(SQLSubModel): def profiles(self, profiles): self._profiles = profiles + @property + def enabled_profiles(self): + return list(filter(lambda p: p.is_enabled, self.profiles)) + + @property + def number_enabled_profiles(self): + return len(self.enabled_profiles) + def get_profiles_from_rk(self, rk): return list( filter( diff --git a/src/Model/Study.py b/src/Model/Study.py index ded212ba..de86b428 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -46,7 +46,7 @@ logger = logging.getLogger() class Study(SQLModel): - _version = "0.2.7" + _version = "0.2.9" _sub_classes = [ Scenario, diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 847722d9..20b8ac01 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -64,15 +64,20 @@ class GeometryReachTableModel(PamhyrTableModel): if not index.isValid(): return QVariant() + profile = self._data.profile(index.row()) + + if role == Qt.BackgroundRole and not profile.is_enabled: + return QColor(190, 190, 190) + if role == Qt.DisplayRole and index.column() == 0: - return self._data.profile(index.row()).name + return profile.name if role == Qt.DisplayRole and index.column() == 1: - rk = self._data.profile(index.row()).rk + rk = profile.rk return f"{rk:.4f}" if role == Qt.DisplayRole and index.column() == 2: - return str(self._data.profile(index.row()).nb_points) + return str(profile.nb_points) if role == Qt.TextAlignmentRole: return Qt.AlignHCenter | Qt.AlignVCenter @@ -155,6 +160,14 @@ class GeometryReachTableModel(PamhyrTableModel): self.endRemoveRows() self.layoutChanged.emit() + def enabled(self, rows, enabled): + self._undo.push( + SetEnabledCommand( + self._data, rows, enabled + ) + ) + self.layoutChanged.emit() + def sort_profiles(self, _reverse): self.layoutAboutToBeChanged.emit() diff --git a/src/View/Geometry/Translate.py b/src/View/Geometry/Translate.py index 1b3702da..c7e74e62 100644 --- a/src/View/Geometry/Translate.py +++ b/src/View/Geometry/Translate.py @@ -44,6 +44,9 @@ class GeometryTranslate(MainTranslate): self._dict["cross_sections"] = _translate("Geometry", "cross-sections") self._dict["profile"] = _translate("Geometry", "cross-section") self._dict["profiles"] = _translate("Geometry", "cross-sections") + self._dict["cross_section_enabled_count"] = _translate( + "Geometry", "Cross-section enabled ({count} disabled)" + ) self._dict["transverse_abscissa"] = _translate( "Geometry", "Transverse abscissa (m)" diff --git a/src/View/Geometry/UndoCommand.py b/src/View/Geometry/UndoCommand.py index 71e9c313..f14bfef8 100644 --- a/src/View/Geometry/UndoCommand.py +++ b/src/View/Geometry/UndoCommand.py @@ -68,6 +68,27 @@ class SetRKCommand(SetDataCommand): self._reach.profile(self._index).rk = self._new +class SetEnabledCommand(QUndoCommand): + def __init__(self, reach, rows, enabled): + QUndoCommand.__init__(self) + + self._profiles = [reach.profile(row) for row in rows] + self._old = [profile.is_enabled for profile in self._profiles] + self._new = bool(enabled) + + @staticmethod + def _set_enabled(profile, enabled): + profile.enabled = enabled + + def undo(self): + for profile, enabled in zip(self._profiles, self._old): + self._set_enabled(profile, enabled) + + def redo(self): + for profile in self._profiles: + self._set_enabled(profile, self._new) + + class AddCommand(QUndoCommand): def __init__(self, reach, index): QUndoCommand.__init__(self) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 19e84363..f96121ca 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -99,6 +99,7 @@ class GeometryWindow(PamhyrWindow): self._profile_window = [] self.setup_table() + self.setup_checkbox() self.setup_plots() self.setup_statusbar() self.setup_connections() @@ -124,6 +125,10 @@ class GeometryWindow(PamhyrWindow): table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) + def setup_checkbox(self): + self._checkbox = self.find(QCheckBox, "checkBox_enabled") + self._set_checkbox_state() + def setup_plots(self): self.setup_plots_xy() self.setup_plots_rkc() @@ -223,6 +228,9 @@ class GeometryWindow(PamhyrWindow): .selectionChanged\ .connect(self.select_current_profile) + if not self._study.is_read_only(): + self._checkbox.clicked.connect(self._set_profiles_state) + self._table.layoutChanged.connect(self.update_redraw) def update(self): @@ -525,6 +533,48 @@ class GeometryWindow(PamhyrWindow): self._plot_ac.draw() self.tableView.model().blockSignals(False) + self._set_checkbox_state() + + def index_selected_rows(self): + return [ + index.row() + for index in self.tableView.selectionModel().selectedRows() + ] + + def _set_checkbox_state(self): + rows = self.index_selected_rows() + editable = not self._study.is_read_only() + disabled_count = self._reach.number_profiles - self._reach.number_enabled_profiles + self._checkbox.setText( + self._trad["cross_section_enabled_count"].format( + count=disabled_count + ) + ) + + self._checkbox.setEnabled(editable and len(rows) > 0) + if len(rows) == 0: + self._checkbox.setTristate(False) + self._checkbox.setChecked(False) + return + + states = { + self._reach.profile(row).is_enabled + for row in rows + } + if len(states) == 1: + self._checkbox.setTristate(False) + self._checkbox.setChecked(states.pop()) + else: + self._checkbox.setTristate(True) + self._checkbox.setCheckState(Qt.PartiallyChecked) + + def _set_profiles_state(self, enabled): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._checkbox.setTristate(False) + self._table.enabled(rows, enabled) def add(self): if len(self.tableView.selectedIndexes()) == 0: diff --git a/src/View/ui/GeometryReach.ui b/src/View/ui/GeometryReach.ui index 8c80a7bf..c87ed1db 100644 --- a/src/View/ui/GeometryReach.ui +++ b/src/View/ui/GeometryReach.ui @@ -24,10 +24,20 @@ Qt::Horizontal - + + + + + Cross-section enabled (0 disabled) + + + true + + + From 824fbe506edd677ee80445d9cd6206ac0f5022d1 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 21 Aug 2026 17:30:38 +0200 Subject: [PATCH 08/13] Profiles: disabled profiles now affects results for mage, AdisTS and AdisTT --- src/Checker/Mage.py | 7 +++++-- src/Checker/Study.py | 10 +++++++--- src/Model/Geometry/Reach.py | 22 ++++++++++++++++------ src/Model/Results/River/River.py | 28 ++++++++++++++++++++-------- src/Solver/AdisTS.py | 9 ++++++--- src/Solver/AdisTT.py | 9 ++++++--- src/Solver/Mage.py | 22 +++++++++++++++------- 7 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/Checker/Mage.py b/src/Checker/Mage.py index 949e61ae..f7d4d747 100644 --- a/src/Checker/Mage.py +++ b/src/Checker/Mage.py @@ -229,7 +229,11 @@ for each reach" gls = [] for edge in edges: - comp, incomp = edge.reach.compute_guidelines() + profiles = edge.reach.enabled_profiles + comp, incomp = edge.reach.compute_guidelines( + profiles=profiles, + update_cache=False + ) if len(incomp) != 0: self._status = STATUS.WARNING self._summary = "incomplete_guideline" @@ -237,7 +241,6 @@ for each reach" gls.append(comp) - profiles = edge.reach.profiles for profile in profiles: if not profile.has_standard_named_points(): self._status = STATUS.WARNING diff --git a/src/Checker/Study.py b/src/Checker/Study.py index bddfa4a7..7889cf2e 100644 --- a/src/Checker/Study.py +++ b/src/Checker/Study.py @@ -87,7 +87,7 @@ class StudyGeometryChecker(AbstractModelChecker): return False for edge in edges: - if len(edge.reach.profiles) < 2: + if len(edge.reach.enabled_profiles) < 2: summary = "no_geometry_defined" status = STATUS.ERROR ok = False @@ -132,8 +132,12 @@ class StudyInitialConditionsChecker(AbstractModelChecker): return ok ic = river.initial_conditions[reach] - len_ic = len(ic) - len_reach = len(reach) + enabled_profiles = set(reach.enabled_profiles) + len_ic = sum( + data["section"] in enabled_profiles + for data in ic.data + ) + len_reach = len(enabled_profiles) if len_ic < len_reach: self._summary = "initial_condition_missing_profile" diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 04f1144e..db5724f6 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -494,14 +494,22 @@ class Reach(SQLSubModel): ) @timer - def compute_guidelines(self): + def compute_guidelines(self, profiles=None, update_cache=True): """Compute reach guidelines + Args: + profiles: Profiles used to compute the guidelines. If omitted, + all non-deleted profiles of the reach are used. + update_cache: Whether to update the reach guidelines cache. + Returns: Tuple of complete and incomplete guidelines name. """ + if profiles is None: + profiles = self.profiles + # Get all point contained into a guideline - named_points = [profile.named_points() for profile in self.profiles] + named_points = [profile.named_points() for profile in profiles] points_name = list( map( lambda lst: list(map(lambda p: p.name, lst)), @@ -532,11 +540,13 @@ class Reach(SQLSubModel): complete = guide_set - incomplete - # Compute guideline and put data in cache - self._compute_guidelines_cache(guide_set, named_points, - complete, incomplete) + if update_cache: + # Compute guideline and put data in cache + self._compute_guidelines_cache( + guide_set, named_points, complete, incomplete + ) + self.modified() - self.modified() return (complete, incomplete) def _map_guidelines_points(self, func, full=False): diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 6e32b7a3..44cacde7 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -276,7 +276,7 @@ class Profile(SQLSubModel): class Reach(SQLSubModel): _sub_classes = [Profile] - def __init__(self, reach, study, parent, with_init=True): + def __init__(self, reach, study, parent, with_init=True, profiles=None): super(Reach, self).__init__( id=-1, status=study.status, owner_scenario=study.status.scenario.id @@ -287,10 +287,12 @@ class Reach(SQLSubModel): self._reach = reach # Source reach in the study self._profiles = [] if with_init: + if profiles is None: + profiles = reach.profiles self._profiles = list( map( lambda p: Profile(p, self._study, self._parent), - reach.profiles + profiles ) ) @@ -379,7 +381,14 @@ class Reach(SQLSubModel): for i, profile in enumerate(reach.profiles): data["profile"] = profile - new_reach._profiles += [Profile._db_load(execute, data)] + result_profile = Profile._db_load(execute, data) + if len(result_profile) > 0: + new_reach._profiles.append(result_profile) + + new_reach._profile_mask = [ + profile.name[0:8] != 'interpol' + for profile in new_reach._profiles + ] return new_reach @@ -418,10 +427,13 @@ class River(SQLSubModel): def has_reach(self, id): return 0 <= id < len(self._reachs) - def add(self, reach_id): + def add(self, reach_id, profiles=None): reachs = self._study.river.enable_edges() - new = Reach(reachs[reach_id].reach, self._study, self._parent) + new = Reach( + reachs[reach_id].reach, self._study, self._parent, + profiles=profiles + ) self._reachs.append(new) return new @@ -460,9 +472,9 @@ class River(SQLSubModel): for reach in study.river.reachs(): data["reach"] = reach.reach - new_river._reachs.append( - Reach._db_load(execute, data) - ) + result_reach = Reach._db_load(execute, data) + if len(result_reach) > 0: + new_river._reachs.append(result_reach) return new_river diff --git a/src/Solver/AdisTS.py b/src/Solver/AdisTS.py index 4c0c11e7..4a619113 100644 --- a/src/Solver/AdisTS.py +++ b/src/Solver/AdisTS.py @@ -180,7 +180,7 @@ class AdisTS(CommandLineSolver): files.append(str(os.path.join("net", f"{name}.ST"))) cnt_num = 1 - for profile in edge.reach.profiles: + for profile in edge.reach.enabled_profiles: self._export_ST_profile_header( f, files, profile, cnt_num ) @@ -335,7 +335,7 @@ class AdisTS(CommandLineSolver): return coeff.minor, coeff.medium - for j, profile in enumerate(edge.reach.profiles): + for j, profile in enumerate(edge.reach.enabled_profiles): coef_min, coef_moy = get_stricklers_from_rk(profile.rk, lst) f.write( @@ -861,7 +861,10 @@ class AdisTSwc(AdisTS): for i in range(ibmax): # Add results reach to reach list - r = results.river.add(i) + geometry_reach = study.river.enable_edges()[i].reach + r = results.river.add( + i, profiles=geometry_reach.enabled_profiles + ) reachs.append(r) is1[i] = data[2 * i] - 1 # first section of reach i diff --git a/src/Solver/AdisTT.py b/src/Solver/AdisTT.py index 418a845d..7605ed13 100644 --- a/src/Solver/AdisTT.py +++ b/src/Solver/AdisTT.py @@ -187,7 +187,7 @@ class AdisTT(CommandLineSolver): files.append(str(os.path.join("net", f"{name}.ST"))) cnt_num = 1 - for profile in edge.reach.profiles: + for profile in edge.reach.enabled_profiles: self._export_ST_profile_header( f, files, profile, cnt_num ) @@ -310,7 +310,7 @@ class AdisTT(CommandLineSolver): return coeff.minor, coeff.medium - for j, profile in enumerate(edge.reach.profiles): + for j, profile in enumerate(edge.reach.enabled_profiles): coef_min, coef_moy = get_stricklers_from_rk(profile.rk, lst) f.write( @@ -647,7 +647,10 @@ class AdisTTwc(AdisTT): for i in range(ibmax): # Add results reach to reach list - r = results.river.add(i) + geometry_reach = study.river.enable_edges()[i].reach + r = results.river.add( + i, profiles=geometry_reach.enabled_profiles + ) reachs.append(r) is1[i] = data[2 * i] - 1 # first section of reach i diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index a7f5740d..c15b2152 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -224,7 +224,7 @@ class Mage(CommandLineSolver): files.append(str(os.path.join("net", f"{name}.ST"))) cnt_num = 1 - for profile in edge.reach.profiles: + for profile in edge.reach.enabled_profiles: self._export_ST_profile_header( f, files, profile, cnt_num ) @@ -489,7 +489,12 @@ class Mage(CommandLineSolver): if cond.is_deleted(): continue - data = cond.data + enabled_profiles = set(reach.reach.enabled_profiles) + data = [ + item for item in cond.data + if not item.is_deleted() + and item["section"] in enabled_profiles + ] if len(data) == 0: continue @@ -497,9 +502,6 @@ class Mage(CommandLineSolver): id_sec = 1 for d in data: - if d.is_deleted(): - continue - IR = f"{id}" IS = f"{id_sec}" discharge = f"{d['discharge']:>10.5f}" @@ -1097,7 +1099,10 @@ class Mage8(Mage): for i in range(nb_reach): # Add results reach to reach list - r = results.river.add(i) + geometry_reach = study.river.enable_edges()[i].reach + r = results.river.add( + i, profiles=geometry_reach.enabled_profiles + ) reachs.append(r) # ID of first and last reach profiles @@ -1402,7 +1407,10 @@ class Mage8(Mage): zfd_lst = [] for r in reachs: - z_min = r.geometry.get_z_min() + z_min = [ + profile.geometry.z_min() + for profile in r.profiles + ] sls = map( lambda p: p.get_ts_key(ts_list[0], "sl")[0], r.profiles From f08d4b1a37fc8c7186aebb15bae01fef90d827b9 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 24 Aug 2026 14:17:39 +0200 Subject: [PATCH 09/13] Disable profiles: results only take into account enabled profiles to display results for Mage --- src/Model/Geometry/Reach.py | 33 ++++++++++++++++++++++ src/Model/Results/Results.py | 2 +- src/Model/Results/River/River.py | 1 + src/View/Results/PlotRKC.py | 48 ++++++++++++++++++-------------- 4 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index db5724f6..cb96699e 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -375,6 +375,28 @@ class Reach(SQLSubModel): ) ) + def get_z_min_enabled_profiles(self): + return list( + map( + lambda profile: profile.z_min(), + filter( + lambda profile: len(profile) > 0 and profile.is_enabled, + self.profiles + ) + ) + ) + + def get_z_max_enabled_profiles(self): + return list( + map( + lambda profile: profile.z_max(), + filter( + lambda profile: len(profile) > 0 and profile.is_enabled, + self.profiles + ) + ) + ) + def get_rk(self): """List of profiles rk @@ -383,6 +405,17 @@ class Reach(SQLSubModel): """ return [profile.rk for profile in self.profiles] + def get_rk_enabled_profiles(self): + return list( + map( + lambda profile: profile.rk, + filter( + lambda profile: len(profile) > 0 and profile.is_enabled, + self.profiles + ) + ) + ) + def get_rk_complete_profiles(self): return list( map( diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 2ea30e03..57e5b17d 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -527,7 +527,7 @@ class Results(SQLSubModel): i = 0 for reach in new_results._river.reachs: nb = len(reach.profiles) - reach.set_global_index(range(i, i + nb + 1)) + reach.set_global_index(range(i, i + nb)) i += nb if "Z" in table_data: diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 44cacde7..cd6d591b 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -154,6 +154,7 @@ class Profile(SQLSubModel): "len_data, data, scenario " + "FROM results_data " + f"WHERE scenario = {scenario.id} " + + f"AND result = {data['result_pid']} " + f"AND reach = {reach.pamhyr_id} " + f"AND section = {profile.pamhyr_id}" ) diff --git a/src/View/Results/PlotRKC.py b/src/View/Results/PlotRKC.py index dc658c65..8c8f3c40 100644 --- a/src/View/Results/PlotRKC.py +++ b/src/View/Results/PlotRKC.py @@ -62,6 +62,8 @@ class PlotRKC(PamhyrPlot): self._auto_relim_update = True self._autoscale_update = False + self.profile = None + @property def results(self): return self.data @@ -71,9 +73,19 @@ class PlotRKC(PamhyrPlot): self.data = results self._current_timestamp = max(self._timestamps) + @staticmethod + def reach_geometry(reach): + """Return geometry values for profiles present in the results.""" + return ( + reach.geometry.get_rk_enabled_profiles(), + reach.geometry.get_z_min_enabled_profiles(), + reach.geometry.get_z_max_enabled_profiles() + ) + @timer def draw(self, highlight=None): self.init_axes() + self.profile = None if self.results is None: return @@ -104,7 +116,7 @@ class PlotRKC(PamhyrPlot): def draw_bottom_with_bedload(self, reach): results = self.results[self._current_res_id] - rk = reach.geometry.get_rk() + rk, _, _ = self.reach_geometry(reach) table = results.get("table")["zfd"] ts = results.get_timestamp_id(self._current_timestamp) @@ -136,8 +148,7 @@ class PlotRKC(PamhyrPlot): for hs in lhs: x = hs.input_section.rk - z_min = reach.geometry.get_z_min() - z_max = reach.geometry.get_z_max() + _, z_min, z_max = self.reach_geometry(reach) self.canvas.axes.plot( [x, x], @@ -157,9 +168,7 @@ class PlotRKC(PamhyrPlot): ) def draw_bottom_geometry(self, reach): - rk = reach.geometry.get_rk() - z_min = reach.geometry.get_z_min() - z_max = reach.geometry.get_z_max() + rk, z_min, _ = self.reach_geometry(reach) self.line_rk_zmin = self.canvas.axes.plot( rk, z_min, @@ -170,10 +179,9 @@ class PlotRKC(PamhyrPlot): self._river_bottom = z_min def draw_water_elevation(self, reach): - if len(reach.geometry.profiles) != 0: + if len(reach.profiles) != 0: result = self.results[self._current_res_id] - rk = reach.geometry.get_rk() - z_min = reach.geometry.get_z_min() + rk, _, _ = self.reach_geometry(reach) table = result.get("table")["Z"] ts = result.get_timestamp_id(self._current_timestamp) @@ -201,10 +209,9 @@ class PlotRKC(PamhyrPlot): ) def draw_water_elevation_max(self, reach): - if len(reach.geometry.profiles) != 0: + if len(reach.profiles) != 0: result = self.results[self._current_res_id] - rk = reach.geometry.get_rk() - z_min = reach.geometry.get_z_min() + rk, _, _ = self.reach_geometry(reach) table = result.get("table")["Z"] ts = result.get_timestamp_id(self._current_timestamp) @@ -250,9 +257,7 @@ class PlotRKC(PamhyrPlot): self.canvas.draw_idle() def draw_current(self, reach): - rk = reach.geometry.get_rk() - z_min = reach.geometry.get_z_min() - z_max = reach.geometry.get_z_max() + rk, z_min, z_max = self.reach_geometry(reach) self.profile, = self.canvas.axes.plot( [ @@ -349,8 +354,7 @@ class PlotRKC(PamhyrPlot): def update_water_elevation(self): result = self.results[self._current_res_id] reach = result.river.reach(self._current_reach_id) - rk = reach.geometry.get_rk() - z_min = reach.geometry.get_z_min() + rk, _, _ = self.reach_geometry(reach) table = result.get("table")["Z"] ts = result.get_timestamp_id(self._current_timestamp) @@ -380,11 +384,13 @@ class PlotRKC(PamhyrPlot): self.update_idle() def update_current(self): + if not self._init or self.profile is None: + self.draw() + return + results = self.results[self._current_res_id] reach = results.river.reach(self._current_reach_id) - rk = reach.geometry.get_rk() - z_min = reach.geometry.get_z_min() - z_max = reach.geometry.get_z_max() + rk, z_min, z_max = self.reach_geometry(reach) cid = self._current_profile_id self.profile.set_data( @@ -396,7 +402,7 @@ class PlotRKC(PamhyrPlot): def update_bottom_with_bedload(self): results = self.results[self._current_res_id] reach = results.river.reach(self._current_reach_id) - rk = reach.geometry.get_rk() + rk, _, _ = self.reach_geometry(reach) # z = self.sl_compute_current_z(reach) table = results.get("table")["zfd"] From 8978cc348245210da697403fa1904504950e7ec8 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 24 Aug 2026 15:37:03 +0200 Subject: [PATCH 10/13] Disabled profiles: the reach split is impossible on a disabled profile to avoid any problem --- src/View/Network/ProfileDialog.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/View/Network/ProfileDialog.py b/src/View/Network/ProfileDialog.py index 28caa99d..446327e7 100644 --- a/src/View/Network/ProfileDialog.py +++ b/src/View/Network/ProfileDialog.py @@ -64,6 +64,7 @@ class SelectProfileDialog(PamhyrDialog): self._reach = reach self._profile = None + self._profiles = self._reach.reach.enabled_profiles self.setup_combobox() @@ -73,17 +74,19 @@ class SelectProfileDialog(PamhyrDialog): list( map( lambda p: p.display_name(), - self._reach.reach.profiles))) + self._profiles))) + + if len(self._profiles) == 0: + self.find(QDialogButtonBox, "buttonBox")\ + .button(QDialogButtonBox.Ok)\ + .setEnabled(False) def accept(self): - profile = self.get_combobox_text("comboBox") + index = self.find(QComboBox, "comboBox").currentIndex() + if not 0 <= index < len(self._profiles): + return - self._profile = next( - filter( - lambda p: p.display_name() == profile, - self._reach.reach.profiles - ) - ) + self._profile = self._profiles[index] super(SelectProfileDialog, self).accept() From 944f99f14b00e1fa582deb6c3aa837fe245b66e3 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 24 Aug 2026 17:01:16 +0200 Subject: [PATCH 11/13] Disable profiles: security to prevent the disable of a profile associated with data (initial conditions, structures, lateral conditions, weather params, init. cond. temperature) --- .../InitialConditionsTemperature.py | 6 +- src/Model/River.py | 57 ++++++++++++++++++ src/View/Geometry/Translate.py | 58 +++++++++++++++++++ src/View/Geometry/Window.py | 43 ++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) diff --git a/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py b/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py index 33e2c99e..52afdb4e 100644 --- a/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py +++ b/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py @@ -167,7 +167,11 @@ class InitialConditionsTemperature(SQLSubModel): return True def __len__(self): - return len(self._data) + return len(self.data) + + @property + def data(self): + return list(filter(lambda item: not item.is_deleted(), self._data)) @property def name(self): diff --git a/src/Model/River.py b/src/Model/River.py index 35e4416f..54af4bc9 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -907,6 +907,63 @@ Last export at: @date.""" def weather_parameters(self): return self._WeatherParameters + def profile_dependencies(self, profile): + """Return model data which directly reference *profile*. + + PK-based intervals are deliberately not included: only explicit + references to the profile object prevent it from being disabled. + """ + dependencies = [] + + def add(kind, item, association): + dependencies.append({ + "kind": kind, + "item": item, + "association": association, + }) + + for reach in list(self._initial_conditions._dict): + initial_conditions = self._initial_conditions.get(reach) + for data in initial_conditions.data: + if data["section"] is profile: + add("initial_condition", data, "section") + + for structure in self._hydraulic_structures.lst: + if structure.input_section is profile: + add("hydraulic_structure", structure, "input_section") + if structure.output_section is profile: + add("hydraulic_structure", structure, "output_section") + + for tab in self._lateral_contribution._tabs_list: + for contribution in self._lateral_contribution.get_tab(tab): + if contribution.begin_section is profile: + add("lateral_contribution", contribution, "begin_section") + if contribution.end_section is profile: + add("lateral_contribution", contribution, "end_section") + + for parameter in self._WeatherParameters.lst: + if parameter.begin_section is profile: + add("weather_parameter", parameter, "begin_section") + if parameter.end_section is profile: + add("weather_parameter", parameter, "end_section") + + reach_id = profile.reach.pamhyr_id + for initial_condition in self._InitialConditionsTemperature.lst: + for specification in initial_condition.data: + same_reach = specification.reach == reach_id + same_rk = ( + specification.start_rk == profile.rk or + specification.end_rk == profile.rk + ) + if same_reach and same_rk: + add( + "initial_condition_temperature", + specification, + "cross_section_pk", + ) + + return dependencies + def get_params(self, solver): if solver in self._parameters: return self._parameters[solver] diff --git a/src/View/Geometry/Translate.py b/src/View/Geometry/Translate.py index c7e74e62..a0f9682a 100644 --- a/src/View/Geometry/Translate.py +++ b/src/View/Geometry/Translate.py @@ -19,6 +19,7 @@ from PyQt5.QtCore import QCoreApplication from View.Translate import MainTranslate +from View.WeatherParameters.translate import WeatherParametersTranslate _translate = QCoreApplication.translate @@ -47,6 +48,63 @@ class GeometryTranslate(MainTranslate): self._dict["cross_section_enabled_count"] = _translate( "Geometry", "Cross-section enabled ({count} disabled)" ) + self._dict["profile_disable_blocked"] = _translate( + "Geometry", "The cross-section cannot be disabled." + ) + self._dict["profile_disable_blocked_info"] = _translate( + "Geometry", + "It is referenced by the following data:\n{dependencies}\n\n" + "Change or remove these associations before disabling it." + ) + self._dict["profile_dependency"] = _translate( + "Geometry", "{kind} “{name}” ({association})" + ) + self._dict["initial_condition"] = _translate( + "Geometry", "Initial condition" + ) + self._dict["initial_condition_temperature"] = _translate( + "Geometry", "Initial condition temperature" + ) + self._dict["hydraulic_structure"] = _translate( + "Geometry", "Hydraulic structure" + ) + self._dict["lateral_contribution"] = _translate( + "Geometry", "Lateral contribution" + ) + self._dict["weather_parameter"] = _translate( + "Geometry", "Weather parameter" + ) + self._dict["section"] = _translate("Geometry", "cross-section") + self._dict["input_section"] = _translate( + "Geometry", "input cross-section" + ) + self._dict["output_section"] = _translate( + "Geometry", "output cross-section" + ) + self._dict["begin_section"] = _translate( + "Geometry", "first cross-section" + ) + self._dict["end_section"] = _translate( + "Geometry", "last cross-section" + ) + self._dict["cross_section_pk"] = _translate( + "Geometry", "cross-section PK" + ) + + weather_parameters = WeatherParametersTranslate().get_dict( + "weather_parameters" + ) + self._sub_dict["weather_parameter_types"] = { + "AT": weather_parameters["air_temperature"], + "SH": weather_parameters["specific_humidity"], + "GR": weather_parameters["global_radiation"], + "RWS": weather_parameters["reference_wind_speed"], + "GT": weather_parameters["surface_temperature"], + "GFR": weather_parameters["surface_flow_rate"], + "ALB": weather_parameters["albedo"], + "SC": weather_parameters["shading_coefficient"], + "CCF": weather_parameters["cloud_cover_fraction"], + } self._dict["transverse_abscissa"] = _translate( "Geometry", "Transverse abscissa (m)" diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index f96121ca..a4c070a4 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -573,6 +573,49 @@ class GeometryWindow(PamhyrWindow): if len(rows) == 0: return + if not enabled: + blocked = [] + for row in rows: + profile = self._reach.profile(row) + dependencies = self._study.river.profile_dependencies(profile) + if len(dependencies) != 0: + blocked.append((profile, dependencies)) + + if len(blocked) != 0: + details = [] + for profile, dependencies in blocked: + details.append(f"{profile.name}:") + for dependency in dependencies: + item = dependency["item"] + if dependency["kind"] == "weather_parameter": + name = self._trad.get_dict( + "weather_parameter_types" + ).get(item.type, item.type) + else: + name = getattr(item, "name", "") + if name == "": + name = self._trad[dependency["kind"]] + details.append( + " - " + + self._trad["profile_dependency"].format( + kind=self._trad[dependency["kind"]], + name=name, + association=self._trad[ + dependency["association"] + ], + ) + ) + + self._set_checkbox_state() + self.message_box( + self._trad["profile_disable_blocked"], + self._trad["profile_disable_blocked_info"].format( + dependencies="\n".join(details) + ), + window_title=self._trad["warning"], + ) + return + self._checkbox.setTristate(False) self._table.enabled(rows, enabled) From da28bae6c556903668bf0e74121b8e0870f39843 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 24 Aug 2026 17:08:37 +0200 Subject: [PATCH 12/13] Fix pep8 --- src/Model/Geometry/ProfileXYZ.py | 5 +++-- src/View/Geometry/Profile/Window.py | 1 - src/View/Geometry/Window.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index e2d52da0..dc6b0c87 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -155,7 +155,7 @@ class ProfileXYZ(Profile, SQLSubModel): "ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE" ) - if major == "0" and int(minor) <=2: + if major == "0" and int(minor) <= 2: if int(release) < 9: execute( "ALTER TABLE geometry_profileXYZ " + @@ -335,7 +335,8 @@ class ProfileXYZ(Profile, SQLSubModel): f"{self.pamhyr_id}, {self._db_format(self.is_deleted())}, " + f"{ind}, '{self._db_format(self._name)}', " + f"{self.reach.pamhyr_id}, {self.rk}, {self.num}, " + - f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}, {self._db_format(self._enabled)}" + + f"{self.code1}, {self.code1}, {sl}, {self._status.scenario_id}, " + + f"{self._db_format(self._enabled)}" + ")" ) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index e59bc963..4ada0795 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -204,7 +204,6 @@ class ProfileWindow(PamhyrWindow): self._tablemodel.beginResetModel() self._tablemodel._data = profile - # update lst to display correct data and apply connexions to add/remove points self._tablemodel._setup_lst() self._tablemodel.endResetModel() diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index a4c070a4..13ab9140 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -544,7 +544,8 @@ class GeometryWindow(PamhyrWindow): def _set_checkbox_state(self): rows = self.index_selected_rows() editable = not self._study.is_read_only() - disabled_count = self._reach.number_profiles - self._reach.number_enabled_profiles + disabled_count = (self._reach.number_profiles - + self._reach.number_enabled_profiles) self._checkbox.setText( self._trad["cross_section_enabled_count"].format( count=disabled_count From 2028d84554f4eaaa2274dab3099d284f6a729465 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Tue, 25 Aug 2026 12:48:02 +0200 Subject: [PATCH 13/13] Result database: fix a problem that happens when we want to open a study with results saved, but results_table_data doesnt exist in database --- src/Model/Results/Results.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 57e5b17d..ebc076b1 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -202,13 +202,8 @@ class TableData(SQLSubModel): @classmethod def _db_update(cls, execute, version, data=None): - major, minor, release = version.strip().split(".") - create = False - - if major == "0" and int(minor) == 2: - if int(release) < 7 and not create: - cls._db_create(execute) - create = True + if not cls.is_table_exists(execute, "results_table_data"): + cls._db_create(execute) return cls._update_submodel(execute, version, data)