diff --git a/src/View/InitialConditionsAdisTS/DialogDischarge.py b/src/View/InitialConditionsAdisTS/DialogDischarge.py new file mode 100644 index 0000000000000000000000000000000000000000..9b63bdabd70c5285cc12ceddef3c5c98861d7c06 --- /dev/null +++ b/src/View/InitialConditionsAdisTS/DialogDischarge.py @@ -0,0 +1,54 @@ +# DialogDischarge.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 View.Tools.PamhyrWindow import PamhyrDialog + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QComboBox, QUndoStack, QShortcut, + QDoubleSpinBox, +) + + +class DischargeDialog(PamhyrDialog): + _pamhyr_ui = "InitialConditions_Dialog_Generator_Discharge" + _pamhyr_name = "Discharge" + + def __init__(self, title="Discharge", trad=None, parent=None): + super(DischargeDialog, self).__init__( + title=trad[self._pamhyr_name], + options=[], + trad=trad, + parent=parent + ) + + self.value = None + + def accept(self): + self.value = self.find(QDoubleSpinBox, "doubleSpinBox").value() + super().accept() + + def reject(self): + self.close() diff --git a/src/View/InitialConditionsAdisTS/DialogHeight.py b/src/View/InitialConditionsAdisTS/DialogHeight.py new file mode 100644 index 0000000000000000000000000000000000000000..408212324c0f47116c97795436f2fe97812d1954 --- /dev/null +++ b/src/View/InitialConditionsAdisTS/DialogHeight.py @@ -0,0 +1,54 @@ +# DialogHeight.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 View.Tools.PamhyrWindow import PamhyrDialog + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QComboBox, QUndoStack, QShortcut, + QDoubleSpinBox, +) + + +class HeightDialog(PamhyrDialog): + _pamhyr_ui = "InitialConditions_Dialog_Generator_Height" + _pamhyr_name = "Height" + + def __init__(self, trad=None, parent=None): + super(HeightDialog, self).__init__( + title=trad[self._pamhyr_name], + options=[], + trad=trad, + parent=parent + ) + + self.value = None + + def accept(self): + self.value = self.find(QDoubleSpinBox, "doubleSpinBox").value() + super().accept() + + def reject(self): + self.close() diff --git a/src/View/InitialConditionsAdisTS/PlotDKP.py b/src/View/InitialConditionsAdisTS/PlotDKP.py new file mode 100644 index 0000000000000000000000000000000000000000..d29356fa01fc1d0b2d9480db5a113189287c078b --- /dev/null +++ b/src/View/InitialConditionsAdisTS/PlotDKP.py @@ -0,0 +1,107 @@ +# PlotDKP.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 +from View.Tools.PamhyrPlot import PamhyrPlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class PlotDKP(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + data=None, parent=None): + super(PlotDKP, self).__init__( + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent + ) + + self.label_x = self._trad["kp"] + self.label_y = self._trad["elevation"] + + self._isometric_axis = False + + self._auto_relim_update = True + self._autoscale_update = True + + @timer + def draw(self, highlight=None): + self.init_axes() + + if self.data is None: + return + + self.draw_river_bottom() + self.draw_water() + + self.idle() + self._init = True + + def draw_river_bottom(self): + kp = self.data.reach.reach.get_kp() + z_min = self.data.reach.reach.get_z_min() + + self.line_kp_zmin = self.canvas.axes.plot( + kp, z_min, + color=self.color_plot_river_bottom, + lw=1. + ) + + def draw_water(self): + if len(self.data) != 0: + kp = self.data.get_kp() + elevation = self.data.get_elevation() + + self.line_kp_elevation = self.canvas.axes.plot( + kp, elevation, + color=self.color_plot_river_water, + **self.plot_default_kargs + ) + + z_min = self.data.reach.reach.get_z_min() + geometry_kp = self.data.reach.reach.get_kp() + + filtred_elevation = list( + map( + lambda x: elevation[x[0]], + filter( + lambda x: x[1] in geometry_kp, + enumerate(kp) + ) + ) + ) + + self.collection = self.canvas.axes.fill_between( + geometry_kp, z_min, filtred_elevation, + color=self.color_plot_river_water_zone, + alpha=0.7, interpolate=True + ) + + @timer + def update(self, ind=None): + self.draw() diff --git a/src/View/InitialConditionsAdisTS/PlotDischarge.py b/src/View/InitialConditionsAdisTS/PlotDischarge.py new file mode 100644 index 0000000000000000000000000000000000000000..7c43c1e557815071c4908de1de08783b1034cc91 --- /dev/null +++ b/src/View/InitialConditionsAdisTS/PlotDischarge.py @@ -0,0 +1,69 @@ +# PlotDischarge.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 + + +class PlotDischarge(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + data=None, parent=None): + super(PlotDischarge, self).__init__( + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent + ) + + self.label_x = self._trad["kp"] + self.label_y = self._trad["discharge"] + + self._isometric_axis = False + + self._auto_relim_update = True + self._autoscale_update = True + + @timer + def draw(self): + self.init_axes() + + if self.data is None: + return + + self.draw_data() + + self.idle() + self._init = True + + def draw_data(self): + kp = self.data.reach.reach.get_kp() + + if len(self.data) != 0: + kp = self.data.get_kp() + discharge = self.data.get_discharge() + + self.line_kp_zmin = self.canvas.axes.plot( + kp, discharge, + color=self.color_plot, + **self.plot_default_kargs + ) + + @timer + def update(self, ind=None): + self.draw() diff --git a/src/View/InitialConditionsAdisTS/Table.py b/src/View/InitialConditionsAdisTS/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..5461e862105ec08b3bb7cf0a9ad0e17210b246b7 --- /dev/null +++ b/src/View/InitialConditionsAdisTS/Table.py @@ -0,0 +1,298 @@ +# 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.InitialConditions.UndoCommand import ( + SetCommand, AddCommand, DelCommand, + SortCommand, MoveCommand, InsertCommand, + DuplicateCommand, GenerateCommand, +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, reach=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._reach = reach.reach + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + self.editor.addItems( + list( + map( + str, + self._reach.get_kp() + ) + ) + ) + + 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 InitialConditionTableModel(PamhyrTableModel): + def __init__(self, reach=None, **kwargs): + self._reach = reach + super(InitialConditionTableModel, self).__init__(**kwargs) + + def _setup_lst(self): + self._lst = self._data.river.initial_conditions.get(self._reach) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] is "speed": + z = self._lst.get(row)["elevation"] + q = self._lst.get(row)["discharge"] + profile = self._reach.reach.get_profiles_from_kp( + self._lst.get(row)["kp"] + ) + if len(profile) >= 1: + speed = profile[0].speed(q, z) + return f"{speed:.4f}" + + return "" + elif self._headers[column] not in ["name", "comment"]: + v = self._lst.get(row)[self._headers[column]] + return f"{v:.4f}" + else: + return self._lst.get(row)[self._headers[column]] + + 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] is not None: + self._undo.push( + SetCommand( + self._lst, row, self._headers[column], 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 sort(self, _reverse, parent=QModelIndex()): + self.layoutAboutToBeChanged.emit() + + self._undo.push( + SortCommand( + self._lst, False + ) + ) + + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() + + def move_up(self, row, parent=QModelIndex()): + if row <= 0: + return + + target = row + 2 + + self.beginMoveRows(parent, row - 1, row - 1, parent, target) + + self._undo.push( + MoveCommand( + self._lst, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, row, parent=QModelIndex()): + if row > len(self._lst): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo.push( + MoveCommand( + self._lst, "down", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def paste(self, index, header, data): + if len(header) != 0: + logger.error("Unexpected header in IC past data") + return + + if len(data) == 0: + logger.error("Empty data") + return + + if len(data[0]) != 3: + logger.error(f"Unexpected data size: [{data[0]}, ...]") + return + + self.layoutAboutToBeChanged.emit() + + self._undo.push( + InsertCommand( + self._lst, index, + list( + map( + lambda d: self._lst.new_from_data(*d), + data + ) + ) + ) + ) + + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() + + def import_from_results(self, index, results): + if results is None: + logger.error("No results data") + return + + self.layoutAboutToBeChanged.emit() + + ts = max(results.get("timestamps")) + res_reach = results.river.get_reach_by_geometry( + self._reach.reach + ) + data = list( + map( + lambda p: [ + p.geometry.kp, + p.get_ts_key(ts, "Q"), + p.get_ts_key(ts, "Z"), + ], + res_reach.profiles + ) + ) + + self._undo.push( + InsertCommand( + self._lst, index, + list( + map( + lambda d: self._lst.new_from_data(*d), + data + ) + ) + ) + ) + + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() + + def generate(self, generator, param): + self._undo.push( + GenerateCommand( + self._lst, generator, param + ) + ) + self.layoutChanged.emit() diff --git a/src/View/InitialConditionsAdisTS/UndoCommand.py b/src/View/InitialConditionsAdisTS/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..fecb6c6d4a0c2f7341047889ee424e4b61876e39 --- /dev/null +++ b/src/View/InitialConditionsAdisTS/UndoCommand.py @@ -0,0 +1,192 @@ +# 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, +) + +from Model.InitialConditions.InitialConditions import InitialConditions +from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict + + +class SetCommand(QUndoCommand): + def __init__(self, ics, row, column, new_value): + QUndoCommand.__init__(self) + + self._ics = ics + self._row = row + self._column = column + self._old = self._ics.get(self._row)[column] + + _type = float + if column == "name" or column == "comment": + _type = str + + self._new = _type(new_value) + + def undo(self): + self._ics.get(self._row)[self._column] = self._old + + def redo(self): + self._ics.get(self._row)[self._column] = self._new + + +class AddCommand(QUndoCommand): + def __init__(self, ics, index): + QUndoCommand.__init__(self) + + self._ics = ics + self._index = index + self._new = None + + def undo(self): + self._ics.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._ics.new(self._index) + else: + self._ics.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, ics, rows): + QUndoCommand.__init__(self) + + self._ics = ics + self._rows = rows + + self._ic = [] + for row in rows: + self._ic.append((row, self._ics.get(row))) + self._ic.sort() + + def undo(self): + for row, el in self._ic: + self._ics.insert(row, el) + + def redo(self): + self._ics.delete_i(self._rows) + + +class SortCommand(QUndoCommand): + def __init__(self, ics, _reverse): + QUndoCommand.__init__(self) + + self._ics = ics + self._reverse = _reverse + + self._old = self._ics.data + self._indexes = None + + def undo(self): + ll = self._ics.data + self._ics.sort( + key=lambda x: self._indexes[ll.index(x)] + ) + + def redo(self): + self._ics.sort( + reverse=self._reverse, + key=lambda x: x["kp"] + ) + if self._indexes is None: + self._indexes = list( + map( + lambda p: self._old.index(p), + self._ics.data + ) + ) + self._old = None + + +class MoveCommand(QUndoCommand): + def __init__(self, ics, up, i): + QUndoCommand.__init__(self) + + self._ics = ics + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._ics.move_up(self._i) + else: + self._ics.move_down(self._i) + + def redo(self): + if self._up: + self._ics.move_up(self._i) + else: + self._ics.move_down(self._i) + + +class InsertCommand(QUndoCommand): + def __init__(self, ics, row, ic): + QUndoCommand.__init__(self) + + self._ics = ics + self._row = row + self._ic = deepcopy(ic) + self._ic.reverse() + + def undo(self): + self._ics.delete(self._ic) + + def redo(self): + for ic in self._ic: + self._ics.insert(self._row, ic) + + +class DuplicateCommand(QUndoCommand): + def __init__(self, ics, rows, ic): + QUndoCommand.__init__(self) + + self._ics = ics + self._rows = rows + self._ic = deepcopy(ic) + self._ic.reverse() + + def undo(self): + self._ics.delete(self._ic) + + def redo(self): + for ic in self._ics: + self._ics.insert(self._rows[0], ic) + + +class GenerateCommand(QUndoCommand): + def __init__(self, ics, generator, param): + QUndoCommand.__init__(self) + + self._ics = ics + self._param = param + self._copy = self._ics.data + self._generator = generator + + def undo(self): + self._ics.data = self._copy + + def redo(self): + if self._generator == "growing": + self._ics.generate_growing_constante_height(self._param) + elif self._generator == "discharge": + self._ics.generate_discharge(self._param) diff --git a/src/View/InitialConditionsAdisTS/Window.py b/src/View/InitialConditionsAdisTS/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..9562f68da0cb7dfcf0299beb13cc3593f87df2c5 --- /dev/null +++ b/src/View/InitialConditionsAdisTS/Window.py @@ -0,0 +1,355 @@ +# 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 os +import logging + +from tools import trace, timer, logger_exception + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +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, QVBoxLayout, QHeaderView, QTabWidget, +) + +from Modules import Modules + +from View.InitialConditionsAdisTS.UndoCommand import ( + SetCommand, AddCommand, DelCommand, + SortCommand, MoveCommand, InsertCommand, + DuplicateCommand, +) + +from View.InitialConditionsAdisTS.Table import ( + InitialConditionTableModel, ComboBoxDelegate, +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.InitialConditionsAdisTS.PlotDKP import PlotDKP +from View.InitialConditionsAdisTS.PlotDischarge import PlotDischarge +from View.InitialConditionsAdisTS.translate import ICTranslate +from View.InitialConditionsAdisTS.DialogHeight import HeightDialog +from View.InitialConditionsAdisTS.DialogDischarge import DischargeDialog +from View.Results.ReadingResultsDialog import ReadingResultsDialog + +from Solver.Mage import Mage8 + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class InitialConditionsWindow(PamhyrWindow): + _pamhyr_ui = "InitialConditions" + _pamhyr_name = "Initial condition" + + def __init__(self, study=None, config=None, reach=None, parent=None): + trad = ICTranslate() + + if reach is not None: + self._reach = reach + else: + self._reach = study.river.current_reach() + + name = ( + trad[self._pamhyr_name] + + " - " + study.name + + " - " + self._reach.name + ) + + super(InitialConditionsWindow, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + # Add reach to hash computation data + self._hash_data.append(self._reach) + + self._ics = study.river.initial_conditions.get(self._reach) + + self.setup_table() + self.setup_plot() + self.setup_connections() + + self.ui.setWindowTitle(self._title) + + def setup_table(self): + table = self.find(QTableView, f"tableView") + self._delegate_kp = ComboBoxDelegate( + reach=self._reach, + parent=self + ) + + self._table = InitialConditionTableModel( + reach=self._reach, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["kp", "discharge", "elevation", "height"], + delegates={"kp": self._delegate_kp}, + data=self._study, + undo=self._undo_stack, + trad=self._trad + ) + + table.setModel(self._table) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_plot(self): + self.canvas_1 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_1.setObjectName("canvas_1") + self.toolbar_1 = PamhyrPlotToolbar( + self.canvas_1, self + ) + self.plot_layout_1 = self.find(QVBoxLayout, "verticalLayout_1") + self.plot_layout_1.addWidget(self.toolbar_1) + self.plot_layout_1.addWidget(self.canvas_1) + + self.plot_1 = PlotDKP( + canvas=self.canvas_1, + data=self._ics, + trad=self._trad, + toolbar=self.toolbar_1, + parent=self + ) + self.plot_1.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_2 = PlotDischarge( + canvas=self.canvas_2, + data=self._ics, + trad=self._trad, + toolbar=self.toolbar_2, + parent=self + ) + self.plot_2.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_del").triggered.connect(self.delete) + self.find(QAction, "action_sort").triggered.connect(self.sort) + self.find(QAction, "action_import").triggered\ + .connect(self.import_from_file) + + self.find(QPushButton, "pushButton_generate_1").clicked.connect( + self.generate_growing_constante_height + ) + + self.find(QPushButton, "pushButton_generate_2").clicked.connect( + self.generate_discharge + ) + + self._table.dataChanged.connect(self._update_plot) + + def index_selected_row(self): + table = self.find(QTableView, f"tableView") + rows = table.selectionModel()\ + .selectedRows() + + if len(rows) == 0: + return 0 + + return rows[0].row() + + def update(self): + self._update_plot() + self._propagate_update(key=Modules.INITIAL_CONDITION) + + def _update_plot(self): + self.plot_1.draw() + self.plot_2.draw() + + def _propagated_update(self, key=Modules(0)): + if Modules.GEOMETRY not in key: + return + + self.update() + + def index_selected_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._ics) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + self._update() + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + self._update() + + def sort(self): + self._table.sort(False) + self._update() + + def import_from_file(self): + workdir = os.path.dirname(self._study.filename) + + return self.file_dialog( + callback=lambda d: self._import_from_file(d[0]), + directory=workdir, + default_suffix=".BIN", + file_filter=["Mage (*.BIN)"], + ) + + def _import_from_file(self, file_name): + solver = Mage8("dummy") + name = os.path.basename(file_name)\ + .replace(".BIN", "") + + def reading(): + self._tmp_results = solver.results( + self._study, + os.path.dirname(file_name), + name=name + ) + + dlg = ReadingResultsDialog( + reading_fn=reading, + parent=self + ) + dlg.exec_() + results = self._tmp_results + self._import_from_results(results) + + def _import_from_results(self, results): + logger.debug(f"import from results: {results}") + row = self.index_selected_row() + + self._table.import_from_results(row, results) + + def move_up(self): + row = self.index_selected_row() + self._table.move_up(row) + self._update() + + def move_down(self): + row = self.index_selected_row() + self._table.move_down(row) + self._update() + + def _copy(self): + rows = list( + map( + lambda row: row.row(), + self.tableView.selectionModel().selectedRows() + ) + ) + + table = list( + map( + lambda eic: list( + map( + lambda k: eic[1][k], + ["kp", "discharge", "elevation"] + ) + ), + filter( + lambda eic: eic[0] in rows, + enumerate(self._ics.lst()) + ) + ) + ) + + self.copyTableIntoClipboard(table) + + def _paste(self): + header, data = self.parseClipboardTable() + + if len(data) + len(header) == 0: + return + + logger.debug( + "IC: Paste: " + + f"header = {header}, " + + f"data = {data}" + ) + + try: + row = self.index_selected_row() + # self._table.paste(row, header, data) + self._table.paste(row, [], data) + except Exception as e: + logger_exception(e) + + self._update() + + def _undo(self): + self._table.undo() + self._update() + + def _redo(self): + self._table.redo() + self._update() + + def generate_growing_constante_height(self): + dlg = HeightDialog(trad=self._trad, parent=self) + if dlg.exec(): + value = dlg.value + self._table.generate("growing", value) + self._update() + + def generate_discharge(self): + dlg = DischargeDialog(trad=self._trad, parent=self) + if dlg.exec(): + value = dlg.value + self._table.generate("discharge", value) + self._update() diff --git a/src/View/InitialConditionsAdisTS/translate.py b/src/View/InitialConditionsAdisTS/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..46d5f4040f3b3f44efad35d6aecd8cdf1a809572 --- /dev/null +++ b/src/View/InitialConditionsAdisTS/translate.py @@ -0,0 +1,49 @@ +# 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 ICTranslate(MainTranslate): + def __init__(self): + super(ICTranslate, self).__init__() + + self._dict["Initial condition"] = _translate( + "InitialCondition", "Initial condition") + self._dict["Discharge"] = _translate( + "InitialCondition", "Discharge") + self._dict["Height"] = _translate( + "InitialCondition", "Height") + + self._dict["elevation"] = self._dict["unit_elevation"] + self._dict["discharge"] = self._dict["unit_discharge"] + self._dict["kp"] = self._dict["unit_kp"] + + self._sub_dict["table_headers"] = { + # "name": _translate("InitialCondition", "Name"), + "kp": self._dict["unit_kp"], + "discharge": self._dict["unit_discharge"], + "elevation": self._dict["unit_elevation"], + "height": self._dict["unit_height"], + "speed": self._dict["unit_speed"], + # "comment": _translate("InitialCondition", "Comment"), + } diff --git a/src/View/Pollutants/Window.py b/src/View/Pollutants/Window.py index a9dc2059c224fd10c85df7c3adad41ad9b503707..cabf5eb773e3c62d450cb43af204f526918b2afe 100644 --- a/src/View/Pollutants/Window.py +++ b/src/View/Pollutants/Window.py @@ -43,6 +43,8 @@ from View.Pollutants.Translate import PollutantsTranslate from View.Pollutants.Edit.Window import EditPolluantWindow +from View.InitialConditionsAdisTS.Window import InitialConditionsWindow + logger = logging.getLogger() @@ -102,6 +104,7 @@ class PollutantsWindow(PamhyrWindow): 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.find(QAction, "action_initial_conditions").triggered.connect(self.initial_conditions) self._checkbox.clicked.connect(self._set_structure_state) table = self.find(QTableView, "tableView") @@ -186,6 +189,27 @@ class PollutantsWindow(PamhyrWindow): ) win.show() + def initial_conditions(self): + if self._study.river.has_current_reach(): + reach = self._study.river.current_reach() + + if self.sub_window_exists( + InitialConditionsWindow, + #data=[self._study, self.conf, reach] + data=[self._study, None, reach] + ): + return + + initial = InitialConditionsWindow( + study=self._study, + #config=self.conf, + reach=reach, + parent=self + ) + initial.show() + else: + self.msg_select_reach() + def _set_checkbox_state(self): row = self.index_selected_row() if row is None: diff --git a/src/View/ui/Pollutant.ui b/src/View/ui/Pollutant.ui index e842bacae69422f03322464664570f854efa5503..ebe58a6f1b6d7ec5b599aa238d01fb67c4342677 100644 --- a/src/View/ui/Pollutant.ui +++ b/src/View/ui/Pollutant.ui @@ -34,9 +34,6 @@ </size> </property> </widget> - <widget class="QWidget" name="verticalLayoutWidget"> - <layout class="QVBoxLayout" name="verticalLayout"/> - </widget> </widget> </item> </layout> diff --git a/src/View/ui/Pollutants.ui b/src/View/ui/Pollutants.ui index 16960d920a52b5b61ee239aab37aed4f6198d81c..9ed80616a81d154a69e8e93c6b283bf719f8c505 100644 --- a/src/View/ui/Pollutants.ui +++ b/src/View/ui/Pollutants.ui @@ -96,6 +96,7 @@ <addaction name="action_add"/> <addaction name="action_delete"/> <addaction name="action_edit"/> + <addaction name="action_initial_conditions"/> </widget> <action name="action_add"> <property name="icon"> @@ -129,6 +130,11 @@ <string>Edit selected hydraulic structure</string> </property> </action> + <action name="action_initial_conditions"> + <property name="text"> + <string>InitialConditions</string> + </property> + </action> </widget> <resources/> <connections/>