diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..26d33521af10bcc7fd8cea344038eaaeb78d0ef5 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..105ce2da2d6447d11dfe32bfb846c3d5b199fc99 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <settings> + <option name="USE_PROJECT_PROFILE" value="false" /> + <version value="1.0" /> + </settings> +</component> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..94c069ce408206d6c87cf00b7f89a3b4d8512b68 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Black"> + <option name="sdkName" value="Python 3.10 (pamhyr)" /> + </component> + <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (pamhyr)" project-jdk-type="Python SDK" /> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..8ad567a589205c549c5c5fd2c87d7b60fb8e2c7b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/pamhyr.iml" filepath="$PROJECT_DIR$/.idea/pamhyr.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/pamhyr.iml b/.idea/pamhyr.iml new file mode 100644 index 0000000000000000000000000000000000000000..c422cdfc5a6269c23ac82259efc8958bc893c067 --- /dev/null +++ b/.idea/pamhyr.iml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <excludeFolder url="file://$MODULE_DIR$/venv" /> + </content> + <orderEntry type="jdk" jdkName="Python 3.10 (pamhyr)" jdkType="Python SDK" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> + <component name="PyDocumentationSettings"> + <option name="format" value="PLAIN" /> + <option name="myDocStringFormat" value="Plain" /> + </component> +</module> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/mage8/mage b/mage8/mage new file mode 100755 index 0000000000000000000000000000000000000000..dc109d6501bed63680fc49d3ff39bfe0fcadd0f5 Binary files /dev/null and b/mage8/mage differ diff --git a/mage8/mage_as7 b/mage8/mage_as7 new file mode 100755 index 0000000000000000000000000000000000000000..74f79fc89126373f12b6a3439125a1d53ebe0a56 Binary files /dev/null and b/mage8/mage_as7 differ diff --git a/mage8/mage_extraire b/mage8/mage_extraire new file mode 100755 index 0000000000000000000000000000000000000000..5526370e8ef023fe7b011791a7f39fa567e7e1fc Binary files /dev/null and b/mage8/mage_extraire differ diff --git a/mage8/mage_extraire_gra b/mage8/mage_extraire_gra new file mode 100755 index 0000000000000000000000000000000000000000..07c594dc62cfe6295ea20645bbfcea3a3ffa5c39 Binary files /dev/null and b/mage8/mage_extraire_gra differ diff --git a/mage8/mailleurTT b/mage8/mailleurTT new file mode 100755 index 0000000000000000000000000000000000000000..5ea642d7f6c9c4a483123778461c7b8186ad4895 Binary files /dev/null and b/mage8/mailleurTT differ diff --git a/src/.pamhyr b/src/.pamhyr new file mode 100644 index 0000000000000000000000000000000000000000..f84c9cfc61d1518be825460524be475ce771ef7a Binary files /dev/null and b/src/.pamhyr differ diff --git a/src/Model/OutputKpAdists/OutputKpAdists.py b/src/Model/OutputKpAdists/OutputKpAdists.py index bb3cc185c4cfe2bc6239cf73ffaaa21dd0c51194..f27332b108bb99e464abc18ca1dbea3db08c6014 100644 --- a/src/Model/OutputKpAdists/OutputKpAdists.py +++ b/src/Model/OutputKpAdists/OutputKpAdists.py @@ -101,8 +101,8 @@ class OutputKpAdists(SQLSubModel): def _db_load(cls, execute, data=None): new = [] - reach = data["reach"] - profile = data["profile"] + #reach = data["reach"] + #profile = data["profile"] status = data["status"] table = execute( diff --git a/src/Model/OutputKpAdists/OutputKpListAdists.py b/src/Model/OutputKpAdists/OutputKpListAdists.py index 331f08374b7a131870922664d3c50ab1f7736461..95886137ce62990f07ab0d2cc507483ab2003ec9 100644 --- a/src/Model/OutputKpAdists/OutputKpListAdists.py +++ b/src/Model/OutputKpAdists/OutputKpListAdists.py @@ -24,7 +24,7 @@ from Model.OutputKpAdists.OutputKpAdists import OutputKpAdists class OutputKpAdistsList(PamhyrModelList): - _sub_classes = [OutputKpAdistsList] + _sub_classes = [OutputKpAdists] @classmethod def _db_load(cls, execute, data=None): diff --git a/src/Model/River.py b/src/Model/River.py index 55c17c634b72808e3c0a4e3ad3a5d173ae8783d7..cb4141797d6f57ba701fc754b56f45e0c597e35d 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -45,6 +45,8 @@ from Model.REPLine.REPLineList import REPLineList from Solver.Solvers import solver_type_list +from Model.OutputKpAdists.OutputKpListAdists import OutputKpAdistsList + class RiverNode(Node, SQLSubModel): _sub_classes = [] @@ -228,6 +230,7 @@ class River(Graph, SQLSubModel): HydraulicStructureList, AddFileList, REPLineList, + OutputKpAdistsList, ] def __init__(self, status=None): @@ -251,6 +254,7 @@ class River(Graph, SQLSubModel): ) self._additional_files = AddFileList(status=self._status) self._rep_lines = REPLineList(status=self._status) + self._Output_kp_adists = OutputKpAdistsList(status=self._status) @classmethod def _db_create(cls, execute): @@ -325,6 +329,10 @@ class River(Graph, SQLSubModel): ) new._rep_lines = REPLineList._db_load(execute, data) + new._Output_kp_adists = OutputKpAdistsList._db_load( + execute, data + ) + return new def _db_save(self, execute, data=None): @@ -344,6 +352,8 @@ class River(Graph, SQLSubModel): for solver in self._parameters: objs.append(self._parameters[solver]) + objs.append(self._Output_kp_adists) + self._save_submodel(execute, objs, data) return True @@ -466,6 +476,10 @@ Last export at: @date.""" def parameters(self): return self._parameters + @property + def Output_kp_adists(self): + return self._Output_kp_adists + def get_params(self, solver): if solver in self._parameters: return self._parameters[solver] diff --git a/src/Solver/AdisTS.py b/src/Solver/AdisTS.py index 573df17233ca1666863894f949e7e05874c53641..364f2be40a4695d2b3b57012a13a07deb4a3182a 100644 --- a/src/Solver/AdisTS.py +++ b/src/Solver/AdisTS.py @@ -81,6 +81,10 @@ class AdisTS(CommandLineSolver): name = self._study.name return f"{name}.REP" + def log_file(self): + name = self._study.name + return f"{name}.TRA" + def _export_REP_additional_lines(self, study, rep_file): lines = filter( lambda line: line.is_enabled(), @@ -117,6 +121,25 @@ class AdisTS(CommandLineSolver): return True + ########### + # RESULTS # + ########### + + def read_bin(self, study, repertory, results, qlog=None, name="0"): + return + + @timer + def results(self, study, repertory, qlog=None, name="0"): + results = Results( + study=study, + solver=self, + repertory=repertory, + name=name, + ) + self.read_bin(study, repertory, results, qlog, name=name) + + return results + ################################ # Adis-TS in low coupling mode # ################################ diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index e1ab44f49d5e4f126659c65f8de4105d9efecccd..448756591c2a10a8bc145a00170ddf7a9990dd2e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -75,6 +75,7 @@ from View.CheckList.Window import CheckListWindow from View.Results.Window import ResultsWindow from View.Results.ReadingResultsDialog import ReadingResultsDialog from View.Debug.Window import ReplWindow +from View.OutputKpAdisTS.Window import OutputKpAdisTSWindow # Optional internal display of documentation for make the application # package lighter... @@ -118,7 +119,7 @@ define_model_action = [ "action_menu_edit_hydraulic_structures", "action_menu_additional_file", "action_menu_results_last", "action_open_results_from_file", "action_menu_boundary_conditions_sediment", - "action_menu_rep_additional_lines", + "action_menu_rep_additional_lines", "action_menu_output_kp", ] action = ( @@ -232,6 +233,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): """ actions = { # Menu action + "action_menu_output_kp": self.open_output_kp_adists, "action_menu_config": self.open_configure, "action_menu_new": self.open_new_study, "action_menu_edit": self.open_edit_study, @@ -860,6 +862,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # SUB WINDOWS # ############### + def open_output_kp_adists(self): + if self.sub_window_exists( + OutputKpAdisTSWindow, + data=[self._study, None] + ): + return + + Output_Kp_AdisTS = OutputKpAdisTSWindow( + study=self._study, + parent=self + ) + Output_Kp_AdisTS.show() + def open_configure(self): """Open configure window diff --git a/src/View/OutputKpAdisTS/BasicHydraulicStructures/Table.py b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..f69a4099f458accc233412f1cfbc4fa6fcf361d4 --- /dev/null +++ b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Table.py @@ -0,0 +1,277 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QMessageBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.HydraulicStructures.BasicHydraulicStructures.UndoCommand import ( + SetNameCommand, SetTypeCommand, + SetEnabledCommand, AddCommand, DelCommand, + SetValueCommand, +) +from Model.HydraulicStructures.Basic.Types import BHS_types + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, trad=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._trad = trad + + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + lst = list( + map( + lambda k: self._long_types[k], + BHS_types.keys() + ) + ) + self.editor.addItems(lst) + + self.editor.setCurrentText(index.data(Qt.DisplayRole)) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(PamhyrTableModel): + def __init__(self, trad=None, **kwargs): + self._trad = trad + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + super(TableModel, self).__init__(trad=trad, **kwargs) + + def rowCount(self, parent): + return len(self._lst) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._data.basic_structure(row).name + elif self._headers[column] == "type": + return self._long_types[self._data.basic_structure(row).type] + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._data, row, value + ) + ) + elif self._headers[column] == "type": + old_type = self._data.basic_structure(row).type + + if old_type == "ND" or self._question_set_type(): + key = next( + k for k, v in self._long_types.items() + if v == value + ) + + self._undo.push( + SetTypeCommand( + self._data, row, BHS_types[key] + ) + ) + except Exception as e: + logger.error(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def _question_set_type(self): + question = QMessageBox(self._parent) + + question.setWindowTitle(self._trad['msg_type_change_title']) + question.setText(self._trad['msg_type_change_text']) + question.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) + question.setIcon(QMessageBox.Question) + + res = question.exec() + return res == QMessageBox.Ok + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._data, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._data, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def enabled(self, row, enabled, parent=QModelIndex()): + self._undo.push( + SetEnabledCommand( + self._lst, row, enabled + ) + ) + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() + + +class ParametersTableModel(PamhyrTableModel): + def __init__(self, trad=None, **kwargs): + self._trad = trad + self._long_types = {} + + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + self._hs_index = None + + super(ParametersTableModel, self).__init__(trad=trad, **kwargs) + + def rowCount(self, parent): + if self._hs_index is None: + return 0 + + return len( + self._data.basic_structure(self._hs_index) + ) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + if self._hs_index is None: + return QVariant() + + row = index.row() + column = index.column() + + hs = self._data.basic_structure(self._hs_index) + + if self._headers[column] == "name": + return self._trad[hs.parameters[row].name] + elif self._headers[column] == "value": + return str(hs.parameters[row].value) + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + if self._hs_index is None: + return QVariant() + + row = index.row() + column = index.column() + + try: + if self._headers[column] == "value": + self._undo.push( + SetValueCommand( + self._data.basic_structure(self._hs_index), + row, value + ) + ) + except Exception as e: + logger.error(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def update_hs_index(self, index): + self._hs_index = index + self.layoutChanged.emit() diff --git a/src/View/OutputKpAdisTS/BasicHydraulicStructures/Translate.py b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Translate.py new file mode 100644 index 0000000000000000000000000000000000000000..f55fb2d778e212588520e94dafd133c101c38413 --- /dev/null +++ b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Translate.py @@ -0,0 +1,155 @@ +# translate.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Translate import MainTranslate + +_translate = QCoreApplication.translate + + +class BasicHydraulicStructuresTranslate(MainTranslate): + def __init__(self): + super(BasicHydraulicStructuresTranslate, self).__init__() + + self._dict["Basic Hydraulic Structures"] = _translate( + "BasicHydraulicStructures", "Basic Hydraulic Structures" + ) + + self._dict['msg_type_change_title'] = _translate( + "BasicHydraulicStructures", + "Change hydraulic structure type" + ) + + self._dict['msg_type_change_text'] = _translate( + "BasicHydraulicStructures", + "Do you want to change the hydraulic structure type and reset \ +hydraulic structure values?" + ) + + # BHSValues translation + + self._dict['width'] = self._dict["unit_width"] + self._dict['height'] = self._dict["unit_thickness"] + self._dict['elevation'] = self._dict["unit_elevation"] + self._dict['diameter'] = self._dict["unit_diameter"] + self._dict['discharge_coefficient'] = _translate( + "BasicHydraulicStructures", "Discharge coefficient" + ) + self._dict['loading_elevation'] = _translate( + "BasicHydraulicStructures", "Upper elevation (m)" + ) + self._dict['half-angle_tangent'] = _translate( + "BasicHydraulicStructures", "Half-angle tangent" + ) + self._dict['maximal_loading_elevation'] = _translate( + "BasicHydraulicStructures", "Maximal loading elevation" + ) + self._dict['siltation_height'] = _translate( + "BasicHydraulicStructures", "Siltation height (m)" + ) + self._dict['top_of_the_vault'] = _translate( + "BasicHydraulicStructures", "Top of the vault (m)" + ) + self._dict['bottom_of_the_vault'] = _translate( + "BasicHydraulicStructures", "Bottom of the vault (m)" + ) + self._dict['opening'] = _translate( + "BasicHydraulicStructures", "Opening" + ) + self._dict['maximal_opening'] = _translate( + "BasicHydraulicStructures", "Maximal opening" + ) + self._dict['step_space'] = _translate( + "BasicHydraulicStructures", "Step space" + ) + self._dict['weir'] = _translate( + "BasicHydraulicStructures", "Weir" + ) + self._dict['coefficient'] = _translate( + "BasicHydraulicStructures", "Coefficient" + ) + + # Dummy parameters + + self._dict['parameter_1'] = _translate( + "BasicHydraulicStructures", "Parameter 1" + ) + self._dict['parameter_2'] = _translate( + "BasicHydraulicStructures", "Parameter 2" + ) + self._dict['parameter_3'] = _translate( + "BasicHydraulicStructures", "Parameter 3" + ) + self._dict['parameter_4'] = _translate( + "BasicHydraulicStructures", "Parameter 4" + ) + self._dict['parameter_5'] = _translate( + "BasicHydraulicStructures", "Parameter 5" + ) + + # BHS types long names + + self._sub_dict["long_types"] = { + "ND": self._dict["not_defined"], + "S1": _translate( + "BasicHydraulicStructures", "Discharge weir" + ), + "S2": _translate( + "BasicHydraulicStructures", "Trapezoidal weir" + ), + "S3": _translate( + "BasicHydraulicStructures", "Triangular weir" + ), + "OR": _translate( + "BasicHydraulicStructures", "Rectangular orifice" + ), + "OC": _translate( + "BasicHydraulicStructures", "Circular orifice" + ), + "OV": _translate( + "BasicHydraulicStructures", "Vaulted orifice" + ), + "V1": _translate( + "BasicHydraulicStructures", "Rectangular gate" + ), + "V2": _translate( + "BasicHydraulicStructures", "Simplified rectangular gate" + ), + "BO": _translate( + "BasicHydraulicStructures", "Borda-type head loss" + ), + "CV": _translate( + "BasicHydraulicStructures", "Check valve" + ), + "UD": _translate( + "BasicHydraulicStructures", "User defined" + ), + } + + # Tables + + self._sub_dict["table_headers"] = { + "name": self._dict["name"], + "type": self._dict["type"], + } + + self._sub_dict["table_headers_parameters"] = { + "name": self._dict["name"], + "value": self._dict["value"], + } diff --git a/src/View/OutputKpAdisTS/BasicHydraulicStructures/UndoCommand.py b/src/View/OutputKpAdisTS/BasicHydraulicStructures/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..78fa3d62ea3f06c45c7d11d4c79b8b7c8b81e6d8 --- /dev/null +++ b/src/View/OutputKpAdisTS/BasicHydraulicStructures/UndoCommand.py @@ -0,0 +1,153 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + + +class SetNameCommand(QUndoCommand): + def __init__(self, hs, index, new_value): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._old = self._hs.basic_structure(self._index).name + self._new = str(new_value) + + def undo(self): + self._hs.basic_structure(self._index).name = self._old + + def redo(self): + self._hs.basic_structure(self._index).name = self._new + + +class SetTypeCommand(QUndoCommand): + def __init__(self, hs, index, new_type): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._type = new_type + self._old = self._hs.basic_structure(self._index) + self._new = self._hs.basic_structure(self._index)\ + .convert(self._type) + + def undo(self): + self._hs.delete_i([self._index]) + self._hs.insert(self._index, self._old) + + def redo(self): + self._hs.delete_i([self._index]) + self._hs.insert(self._index, self._new) + + +class SetEnabledCommand(QUndoCommand): + def __init__(self, hs, index, enabled): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._old = not enabled + self._new = enabled + + def undo(self): + self._hs.basic_structure(self._index).enabled = self._old + + def redo(self): + self._hs.basic_structure(self._index).enabled = self._new + + +class AddCommand(QUndoCommand): + def __init__(self, hs, index): + QUndoCommand.__init__(self) + + self._hs = hs + + self._index = index + self._new = None + + def undo(self): + self._hs.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._hs.add(self._index) + else: + self._hs.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, hs, rows): + QUndoCommand.__init__(self) + + self._hs = hs + + self._rows = rows + + self._bhs = [] + for row in rows: + self._bhs.append((row, self._hs.basic_structure(row))) + + def undo(self): + for row, el in self._bhs: + self._hs.insert(row, el) + + def redo(self): + self._hs.delete_i(self._rows) + + +class PasteCommand(QUndoCommand): + def __init__(self, hs, row, h_s): + QUndoCommand.__init__(self) + + self._hs = hs + + self._row = row + self._bhs = deepcopy(h_s) + self._bhs.reverse() + + def undo(self): + self._hs.delete_i(range(self._row, self._row + len(self._bhs))) + + def redo(self): + for r in self._bhs: + self._hs.insert(self._row, r) + +#################################### +# Basic hydraulic structure values # +#################################### + + +class SetValueCommand(QUndoCommand): + def __init__(self, bhs, index, value): + QUndoCommand.__init__(self) + + self._bhs = bhs + self._index = index + self._old = self._bhs.parameters[self._index].value + self._new = self._bhs.parameters[self._index].type(value) + + def undo(self): + self._bhs.parameters[self._index].value = self._old + + def redo(self): + self._bhs.parameters[self._index].value = self._new diff --git a/src/View/OutputKpAdisTS/BasicHydraulicStructures/Window.py b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..9a39f49c64749b995b3ac3733038ee0e720003b5 --- /dev/null +++ b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Window.py @@ -0,0 +1,270 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from tools import timer, trace + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, QItemSelectionModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.HydraulicStructures.PlotAC import PlotAC + +from View.HydraulicStructures.BasicHydraulicStructures.Table import ( + ComboBoxDelegate, TableModel, ParametersTableModel, +) + +from View.Network.GraphWidget import GraphWidget +from View.HydraulicStructures.BasicHydraulicStructures.Translate import ( + BasicHydraulicStructuresTranslate +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class BasicHydraulicStructuresWindow(PamhyrWindow): + _pamhyr_ui = "BasicHydraulicStructures" + _pamhyr_name = "Basic Hydraulic Structures" + + def __init__(self, data=None, study=None, config=None, parent=None): + trad = BasicHydraulicStructuresTranslate() + name = " - ".join([ + trad[self._pamhyr_name], data.name, study.name + ]) + + super(BasicHydraulicStructuresWindow, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + self._hash_data.append(data) + + self._hs = data + + self.setup_table() + self.setup_checkbox() + self.setup_plot() + self.setup_connections() + + self.update() + + def setup_table(self): + self.setup_table_bhs() + self.setup_table_bhs_parameters() + + def setup_table_bhs(self): + self._table = None + + self._delegate_type = ComboBoxDelegate( + trad=self._trad, + parent=self + ) + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "type"], + delegates={ + "type": self._delegate_type, + }, + trad=self._trad, + data=self._hs, + undo=self._undo_stack, + parent=self, + ) + + selectionModel = table.selectionModel() + index = table.model().index(0, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def setup_table_bhs_parameters(self): + self._table_parameters = None + + table = self.find(QTableView, f"tableView_2") + self._table_parameters = ParametersTableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers_parameters"), + editable_headers=["value"], + delegates={}, + trad=self._trad, + data=self._hs, + undo=self._undo_stack, + parent=self, + ) + + def setup_checkbox(self): + self._checkbox = self.find(QCheckBox, f"checkBox") + self._set_checkbox_state() + + def setup_plot(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) + self.plot_layout.addWidget(self.canvas) + + reach = self._hs.input_reach + profile_kp = self._hs.input_kp + if profile_kp is not None: + profiles = reach.reach.get_profiles_from_kp(float(profile_kp)) + else: + profiles = None + if profiles is not None: + profile = profiles[0] + else: + profile = None + + self.plot_ac = PlotAC( + canvas=self.canvas, + river=self._study.river, + reach=self._hs.input_reach, + profile=profile, + trad=self._trad, + toolbar=self.toolbar + ) + self.plot_ac.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self._checkbox.clicked.connect(self._set_basic_structure_state) + + table = self.find(QTableView, "tableView") + table.selectionModel()\ + .selectionChanged\ + .connect(self.update) + + self._table.dataChanged.connect(self.update) + self._table.layoutChanged.connect(self.update) + + def index_selected(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0] + else: + return None + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0].row() + else: + return None + + def index_selected_rows(self): + table = self.find(QTableView, "tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + + if len(self._hs) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + + if len(rows) == 0: + return + + self._table.delete(rows) + + def _copy(self): + logger.info("TODO: copy") + + def _paste(self): + logger.info("TODO: paste") + + def _undo(self): + self._table.undo() + + def _redo(self): + self._table.redo() + + def _set_checkbox_state(self): + row = self.index_selected_row() + + if row is None: + self._checkbox.setEnabled(False) + self._checkbox.setChecked(True) + else: + self._checkbox.setEnabled(True) + self._checkbox.setChecked(self._hs.basic_structure(row).enabled) + + def _set_basic_structure_state(self): + rows = self.index_selected_rows() + if len(rows) != 0: + for row in rows: + if row is not None: + self._table.enabled( + row, + self._checkbox.isChecked() + ) + + def update(self): + self._set_checkbox_state() + self._update_parameters_table() + + def _update_parameters_table(self): + row = self.index_selected_row() + self._table_parameters.update_hs_index(row) diff --git a/src/View/OutputKpAdisTS/PlotAC.py b/src/View/OutputKpAdisTS/PlotAC.py new file mode 100644 index 0000000000000000000000000000000000000000..c63dc9bfdef5fb2e311b54eb90cf85e4937a4b1c --- /dev/null +++ b/src/View/OutputKpAdisTS/PlotAC.py @@ -0,0 +1,120 @@ +# PlotAC.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from tools import timer +from View.Tools.PamhyrPlot import PamhyrPlot +from matplotlib import pyplot as plt + + +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + river=None, reach=None, profile=None, + parent=None): + super(PlotAC, self).__init__( + canvas=canvas, + trad=trad, + data=river, + toolbar=toolbar, + parent=parent + ) + + self._current_reach = reach + self._current_profile = profile + + self.label_x = self._trad["x"] + self.label_y = self._trad["unit_elevation"] + + self._isometric_axis = False + + self._auto_relim_update = True + self._autoscale_update = True + + @property + def river(self): + return self.data + + @river.setter + def river(self, river): + self.data = river + + @timer + def draw(self): + self.init_axes() + + if self.data is None: + self.line_kp = None + return + + if self._current_reach is None: + self.line_kp = None + return + + self.draw_data() + + self.idle() + self._init = True + + def draw_data(self): + reach = self._current_reach + + if self._current_profile is None: + self.line_kp = None + else: + profile = self._current_profile + x = profile.get_station() + z = profile.z() + + self.line_kp, = self.canvas.axes.plot( + x, z, + color=self.color_plot_river_bottom, + **self.plot_default_kargs + ) + + def set_reach(self, reach): + self._current_reach = reach + self.update() + + def set_profile(self, profile): + self._current_profile = profile + self.update() + + def update(self): + if self.line_kp is None: + self.draw() + return + + if self._current_reach is None or self._current_profile is None: + self.update_clear() + else: + self.update_data() + + self.update_idle() + + def update_data(self): + profile = self._current_profile + x = profile.get_station() + z = profile.z() + + self.line_kp.set_data(x, z) + + def clear(self): + self.update_clear() + + def update_clear(self): + if self.line_kp is not None: + self.line_kp.set_data([], []) diff --git a/src/View/OutputKpAdisTS/PlotKPC.py b/src/View/OutputKpAdisTS/PlotKPC.py new file mode 100644 index 0000000000000000000000000000000000000000..3327f6e627114ac8513dda1ca57670b0cdf722e8 --- /dev/null +++ b/src/View/OutputKpAdisTS/PlotKPC.py @@ -0,0 +1,165 @@ +# PlotKPC.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from tools import timer +from View.Tools.PamhyrPlot import PamhyrPlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +from matplotlib.collections import LineCollection + +_translate = QCoreApplication.translate + + +class PlotKPC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + river=None, reach=None, profile=None, + parent=None): + super(PlotKPC, self).__init__( + canvas=canvas, + trad=trad, + data=river, + toolbar=toolbar, + parent=parent + ) + + self._current_reach = reach + self._current_profile = profile + + self.label_x = self._trad["unit_kp"] + self.label_y = self._trad["unit_elevation"] + + self._isometric_axis = False + + self._auto_relim_update = True + self._autoscale_update = True + + @property + def river(self): + return self.data + + @river.setter + def river(self, river): + self.data = river + + @timer + def draw(self, highlight=None): + self.init_axes() + + if self.data is None: + self.profile = None + self.line_kp_zmin_zmax = None + self.line_kp_zmin = None + return + + if self._current_reach is None: + self.profile = None + self.line_kp_zmin_zmax = None + self.line_kp_zmin = None + return + + self.draw_data() + self.draw_current() + + self.idle() + self._init = True + + def draw_data(self): + reach = self._current_reach + + kp = reach.reach.get_kp() + z_min = reach.reach.get_z_min() + z_max = reach.reach.get_z_max() + + self.line_kp_zmin, = self.canvas.axes.plot( + kp, z_min, + color=self.color_plot_river_bottom, + lw=1. + ) + + if len(kp) != 0: + self.line_kp_zmin_zmax = self.canvas.axes.vlines( + x=kp, + ymin=z_min, ymax=z_max, + color=self.color_plot, + lw=1. + ) + + def draw_current(self): + if self._current_profile is None: + self.profile = None + else: + kp = [self._current_profile.kp, + self._current_profile.kp] + min_max = [self._current_profile.z_min(), + self._current_profile.z_max()] + + self.profile = self.canvas.axes.plot( + kp, min_max, + color=self.color_plot_current, + lw=1. + ) + + def set_reach(self, reach): + self._current_reach = reach + self._current_profile = None + self.update() + + def set_profile(self, profile): + self._current_profile = profile + self.update_current_profile() + + def update(self): + self.draw() + + def update_current_profile(self): + reach = self._current_reach + kp = reach.reach.get_kp() + z_min = reach.reach.get_z_min() + z_max = reach.reach.get_z_max() + + if self.profile is None: + self.draw() + else: + self.profile.set_data( + [self._current_profile.kp, self._current_profile.kp], + [self._current_profile.z_min(), self._current_profile.z_max()], + ) + + self.update_idle() + + def clear(self): + if self.profile is not None: + self.profile[0].set_data([], []) + + if self.line_kp_zmin_zmax is not None: + self.line_kp_zmin_zmax.remove() + self.line_kp_zmin_zmax = None + + if self.line_kp_zmin is not None: + self.line_kp_zmin.set_data([], []) + + self.canvas.figure.canvas.draw_idle() + + def clear_profile(self): + if self.profile is not None: + self.profile.set_data([], []) + + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/OutputKpAdisTS/Table.py b/src/View/OutputKpAdisTS/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..a794f0a0d2ffc9bf14cf14071086d361e042dbfc --- /dev/null +++ b/src/View/OutputKpAdisTS/Table.py @@ -0,0 +1,215 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.HydraulicStructures.UndoCommand import ( + SetNameCommand, SetReachCommand, SetKpCommand, + SetEnabledCommand, AddCommand, DelCommand, +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, trad=None, parent=None, mode="reaches"): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._trad = trad + self._mode = mode + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + val = [] + if self._mode == "kp": + reach = self._data.hydraulic_structures\ + .get(index.row())\ + .input_reach + if reach is not None: + val = list( + map( + lambda kp: str(kp), reach.reach.get_kp() + ) + ) + else: + val = list( + map( + lambda n: n.name, self._data.edges() + ) + ) + + self.editor.addItems( + [self._trad['not_associated']] + + val + ) + + self.editor.setCurrentText(str(index.data(Qt.DisplayRole))) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data._hydraulic_structures + + def rowCount(self, parent): + return len(self._lst) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._lst.get(row).name + elif self._headers[column] == "reach": + n = self._lst.get(row).input_reach + if n is None: + return self._trad['not_associated'] + return n.name + elif self._headers[column] == "kp": + n = self._lst.get(row).input_kp + if n is None: + return self._trad['not_associated'] + return n + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + na = self._trad['not_associated'] + + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._lst, row, value + ) + ) + elif self._headers[column] == "reach": + if value == na: + value = None + + self._undo.push( + SetReachCommand( + self._lst, row, self._data.edge(value) + ) + ) + elif self._headers[column] == "kp": + if value == na: + value = None + + self._undo.push( + SetKpCommand( + self._lst, row, value + ) + ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._lst, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._lst, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def enabled(self, row, enabled, parent=QModelIndex()): + self._undo.push( + SetEnabledCommand( + self._lst, row, enabled + ) + ) + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/OutputKpAdisTS/Translate.py b/src/View/OutputKpAdisTS/Translate.py new file mode 100644 index 0000000000000000000000000000000000000000..b57fb116242da3b1948e8c092878835d139511d7 --- /dev/null +++ b/src/View/OutputKpAdisTS/Translate.py @@ -0,0 +1,40 @@ +# translate.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Translate import MainTranslate + +_translate = QCoreApplication.translate + + +class OutputKpAdisTSTranslate(MainTranslate): + def __init__(self): + super(OutputKpAdisTSTranslate, self).__init__() + + self._dict["Output Kp"] = _translate( + "OutputKpAdisTS", "Output Kp" + ) + + self._dict["x"] = _translate("OutputKpAdisTS", "X (m)") + + self._sub_dict["table_headers"] = { + "name": self._dict["name"], + "reach": self._dict["reach"], + "kp": self._dict["unit_kp"], + } diff --git a/src/View/OutputKpAdisTS/UndoCommand.py b/src/View/OutputKpAdisTS/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..cbb8a2e7927237c3b1a2d97f689c0a547270794d --- /dev/null +++ b/src/View/OutputKpAdisTS/UndoCommand.py @@ -0,0 +1,159 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +logger = logging.getLogger() + + +class SetNameCommand(QUndoCommand): + def __init__(self, h_s_lst, index, new_value): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).name + self._new = str(new_value) + + def undo(self): + self._h_s_lst.get(self._index).name = self._old + + def redo(self): + self._h_s_lst.get(self._index).name = self._new + + +class SetReachCommand(QUndoCommand): + def __init__(self, h_s_lst, index, reach): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).input_reach + self._new = reach + self._old_kp = self._h_s_lst.get(self._index).input_kp + self._new_kp = None + + def undo(self): + i = self._h_s_lst.get(self._index) + i.input_reach = self._old + i.input_kp = self._old_kp + + def redo(self): + i = self._h_s_lst.get(self._index) + i.input_reach = self._new + i.input_kp = self._new_kp + + +class SetKpCommand(QUndoCommand): + def __init__(self, h_s_lst, index, kp): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).input_kp + self._new = kp + + def undo(self): + self._h_s_lst.get(self._index).input_kp = self._old + + def redo(self): + self._h_s_lst.get(self._index).input_kp = self._new + + +class SetEnabledCommand(QUndoCommand): + def __init__(self, h_s_lst, index, enabled): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = not enabled + self._new = enabled + + def undo(self): + self._h_s_lst.get(self._index).enabled = self._old + + def redo(self): + self._h_s_lst.get(self._index).enabled = self._new + + +class AddCommand(QUndoCommand): + def __init__(self, h_s_lst, index): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._index = index + self._new = None + + def undo(self): + self._h_s_lst.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._h_s_lst.new(self._h_s_lst, self._index) + else: + self._h_s_lst.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, h_s_lst, rows): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._rows = rows + + self._h_s = [] + for row in rows: + self._h_s.append((row, self._h_s_lst.get(row))) + self._h_s.sort() + + def undo(self): + for row, el in self._h_s: + self._h_s_lst.insert(row, el) + + def redo(self): + self._h_s_lst.delete_i(self._rows) + + +class PasteCommand(QUndoCommand): + def __init__(self, h_s_lst, row, h_s): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._row = row + self._h_s = deepcopy(h_s) + self._h_s.reverse() + + def undo(self): + self._h_s_lst.delete_i( + self._tab, + range(self._row, self._row + len(self._h_s)) + ) + + def redo(self): + for r in self._h_s: + self._h_s_lst.insert(self._row, r) diff --git a/src/View/OutputKpAdisTS/Window.py b/src/View/OutputKpAdisTS/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..3951f52af2fb979a117025fa0ff6f0a99221bc85 --- /dev/null +++ b/src/View/OutputKpAdisTS/Window.py @@ -0,0 +1,315 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023-2024 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from tools import timer, trace + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, QItemSelectionModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.OutputKpAdisTS.PlotAC import PlotAC +from View.OutputKpAdisTS.PlotKPC import PlotKPC + +from View.OutputKpAdisTS.Table import ( + TableModel, ComboBoxDelegate +) + +from View.Network.GraphWidget import GraphWidget +from View.OutputKpAdisTS.Translate import OutputKpAdisTSTranslate + +from View.OutputKpAdisTS.BasicHydraulicStructures.Window import ( + BasicHydraulicStructuresWindow +) + +logger = logging.getLogger() + + +class OutputKpAdisTSWindow(PamhyrWindow): + _pamhyr_ui = "OutputKpAdisTS" + _pamhyr_name = "Output Kp" + + def __init__(self, study=None, config=None, parent=None): + trad = OutputKpAdisTSTranslate() + name = trad[self._pamhyr_name] + " - " + study.name + + super(OutputKpAdisTSWindow, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + self._hs_lst = self._study.river._hydraulic_structures + + self.setup_table() + #self.setup_checkbox() + #self.setup_plots() + self.setup_connections() + + #self.update() + + def setup_table(self): + self._table = None + + self._delegate_reach = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + parent=self, + mode="reaches" + ) + self._delegate_kp = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + parent=self, + mode="kp" + ) + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "reach", "kp"], + delegates={ + "reach": self._delegate_reach, + "kp": self._delegate_kp, + }, + trad=self._trad, + data=self._study.river, + undo=self._undo_stack, + ) + + selectionModel = table.selectionModel() + index = table.model().index(0, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def setup_checkbox(self): + self._checkbox = self.find(QCheckBox, f"checkBox") + self._set_checkbox_state() + + def setup_plots(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) + self.plot_layout.addWidget(self.canvas) + + self.plot_kpc = PlotKPC( + canvas=self.canvas, + river=self._study.river, + reach=None, + profile=None, + trad=self._trad, + toolbar=self.toolbar + ) + self.plot_kpc.draw() + + self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_2.setObjectName("canvas_2") + self.toolbar_2 = PamhyrPlotToolbar( + self.canvas_2, self + ) + self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout_2.addWidget(self.toolbar_2) + self.plot_layout_2.addWidget(self.canvas_2) + + self.plot_ac = PlotAC( + canvas=self.canvas_2, + river=self._study.river, + reach=None, + profile=None, + trad=self._trad, + toolbar=self.toolbar_2 + ) + self.plot_ac.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self.find(QAction, "action_edit").triggered.connect(self.edit) + self._checkbox.clicked.connect(self._set_structure_state) + + table = self.find(QTableView, "tableView") + table.selectionModel()\ + .selectionChanged\ + .connect(self.update) + + self._table.dataChanged.connect(self.update) + self._table.layoutChanged.connect(self.update) + + def index_selected(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0] + else: + return None + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0].row() + else: + return None + + def index_selected_rows(self): + table = self.find(QTableView, "tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._hs_lst) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + + def _copy(self): + logger.info("TODO: copy") + + def _paste(self): + logger.info("TODO: paste") + + def _undo(self): + self._table.undo() + + def _redo(self): + self._table.redo() + + def edit(self): + rows = self.index_selected_rows() + for row in rows: + data = self._hs_lst.get(row) + + if self.sub_window_exists( + BasicHydraulicStructuresWindow, + data=[self._study, None, data] + ): + continue + + win = BasicHydraulicStructuresWindow( + data=data, + study=self._study, + parent=self + ) + win.show() + + def _set_checkbox_state(self): + row = self.index_selected_row() + if row is None: + self._checkbox.setEnabled(False) + self._checkbox.setChecked(True) + else: + self._checkbox.setEnabled(True) + self._checkbox.setChecked(self._hs_lst.get(row).enabled) + + def _set_structure_state(self): + rows = self.index_selected_rows() + if len(rows) != 0: + for row in rows: + if row is not None: + self._table.enabled( + row, + self._checkbox.isChecked() + ) + + def update(self): + self._set_checkbox_state() + self._update_clear_plot() + + def _update_clear_plot(self): + rows = self.index_selected_rows() + + if len(rows) == 0 or len(self._hs_lst) == 0: + self._update_clear_all() + return + + reach = self._hs_lst.get(rows[0]).input_reach + if reach is not None: + self.plot_kpc.set_reach(reach) + self.plot_ac.set_reach(reach) + + profile_kp = self._hs_lst.get(rows[0]).input_kp + if profile_kp is not None: + profiles = reach.reach\ + .get_profiles_from_kp( + float(profile_kp) + ) + + if len(profiles) != 0 and profiles is not None: + profile = profiles[0] + + self.plot_kpc.set_profile(profile) + self.plot_ac.set_profile(profile) + else: + self._update_clear_profile() + else: + self._update_clear_profile() + else: + self._update_clear_all() + + def _update_clear_all(self): + self.plot_kpc.clear() + self.plot_ac.clear() + + def _update_clear_profile(self): + self.plot_ac.clear() + self.plot_kpc.clear_profile() diff --git a/tests_cases/Enlargement/Enlargement.pamhyr b/tests_cases/Enlargement/Enlargement.pamhyr index 12b3751da32b437e218fa5932fe18c75df6d416f..275fad25b44b7bc23ef14b467d0a1211b777f704 100644 Binary files a/tests_cases/Enlargement/Enlargement.pamhyr and b/tests_cases/Enlargement/Enlargement.pamhyr differ