diff --git a/src/Model/BoundaryConditionsAdisTS/BoundaryConditionAdisTS.py b/src/Model/BoundaryConditionsAdisTS/BoundaryConditionAdisTS.py index b74eb9a5b3acf0534ffd7cefbed7c54fd2389066..4176350c1a56e3acfd5fdc8a08c48567f8799087 100644 --- a/src/Model/BoundaryConditionsAdisTS/BoundaryConditionAdisTS.py +++ b/src/Model/BoundaryConditionsAdisTS/BoundaryConditionAdisTS.py @@ -36,7 +36,7 @@ class BoundaryConditionAdisTS(SQLSubModel): _id_cnt = 0 def __init__(self, id: int = -1, - status=None): + pollutant: int = -1, status=None): super(BoundaryConditionAdisTS, self).__init__() self._status = status @@ -48,7 +48,7 @@ class BoundaryConditionAdisTS(SQLSubModel): self._type = "" self._node = None - self._pollutant = None + self._pollutant = pollutant self._data = [] self._header = [] self._types = [self.time_convert, float] @@ -87,37 +87,39 @@ class BoundaryConditionAdisTS(SQLSubModel): @classmethod def _db_load(cls, execute, data=None): new = [] - pollutant_id = data["pollutant_id"] table = execute( "SELECT id, pollutant, type, node " + - "FROM boundary_condition_adists " + - f"WHERE tab = '{pollutant_id}'" + "FROM boundary_condition_adists" ) - for row in table: - bc = cls( - id=row[0], - status=data['status'] - ) + if table is not None: + for row in table: + bc = cls( + id=row[0], + pollutant=row[1], + status=data['status'] + ) - bc.node = None - if row[3] != -1: - bc.node = next(filter(lambda n: n.id == row[3], data["nodes"])) + bc.type = row[2] - values = execute( - "SELECT data0, data1 FROM boundary_condition_data_adists " + - f"WHERE bc = '{bc.id}'" - ) + bc.node = None + if row[3] != -1: + bc.node = next(filter(lambda n: n.id == row[3], data["nodes"])) - # Write data - for v in values: - data0 = bc._types[0](v[1]) - data1 = bc._types[1](v[2]) - # Replace data at pos ind - bc._data.append((data0, data1)) + values = execute( + "SELECT data0, data1 FROM boundary_condition_data_adists " + + f"WHERE bc = '{bc.id}'" + ) - new.append(bc) + # Write data + for v in values: + data0 = bc._types[0](v[1]) + data1 = bc._types[1](v[2]) + # Replace data at pos ind + bc._data.append((data0, data1)) + + new.append(bc) return new @@ -194,6 +196,11 @@ class BoundaryConditionAdisTS(SQLSubModel): def type(self): return self._type + @type.setter + def type(self, type): + self._type = type + self._status.modified() + @property def data(self): return self._data.copy() diff --git a/src/Model/BoundaryConditionsAdisTS/BoundaryConditionsAdisTSList.py b/src/Model/BoundaryConditionsAdisTS/BoundaryConditionsAdisTSList.py new file mode 100644 index 0000000000000000000000000000000000000000..4246905fd2f6c164801c8efd0ccad0cc02572ac6 --- /dev/null +++ b/src/Model/BoundaryConditionsAdisTS/BoundaryConditionsAdisTSList.py @@ -0,0 +1,63 @@ +# BoundaryConditionsAdisTSList.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 copy +from tools import trace, timer + +from Model.Tools.PamhyrList import PamhyrModelList +from Model.Except import NotImplementedMethodeError + +from Model.BoundaryConditionsAdisTS.BoundaryConditionAdisTS import BoundaryConditionAdisTS + +class BoundaryConditionsAdisTSList(PamhyrModelList): + _sub_classes = [ + BoundaryConditionAdisTS, + ] + + @classmethod + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) + + if data is None: + data = {} + + new._lst = BoundaryConditionAdisTS._db_load( + execute, data + ) + + return new + + def _db_save(self, execute, data=None): + execute("DELETE FROM boundary_condition_adists") + + if data is None: + data = {} + + for bc in self._lst: + bc._db_save(execute, data=data) + + return True + + def new(self, index, pollutant): + n = BoundaryConditionAdisTS(pollutant=pollutant, status=self._status) + self._lst.insert(index, n) + self._status.modified() + return n + + + diff --git a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py index 6430f738e6534cb73612e5559712f545e02c7c4e..e45aba048da24c1e082d114572a5a148532c68fc 100644 --- a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py +++ b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSList.py @@ -41,7 +41,6 @@ class InitialConditionsAdisTSList(PamhyrModelList): return new def _db_save(self, execute, data=None): - print("db save ic adists") execute("DELETE FROM initial_conditions") if data is None: diff --git a/src/Model/River.py b/src/Model/River.py index 96dfb6b01260977e40c9749ebe53a3a8df051986..4b129344dacc56f79280213d2ccd608f9994a9fd 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -48,6 +48,7 @@ from Solver.Solvers import solver_type_list from Model.OutputKpAdists.OutputKpListAdists import OutputKpAdistsList from Model.Pollutants.PollutantsList import PollutantsList from Model.InitialConditionsAdisTS.InitialConditionsAdisTSList import InitialConditionsAdisTSList +from Model.BoundaryConditionsAdisTS.BoundaryConditionsAdisTSList import BoundaryConditionsAdisTSList class RiverNode(Node, SQLSubModel): @@ -235,6 +236,7 @@ class River(Graph, SQLSubModel): OutputKpAdistsList, PollutantsList, InitialConditionsAdisTSList, + BoundaryConditionsAdisTSList, ] def __init__(self, status=None): @@ -261,6 +263,7 @@ class River(Graph, SQLSubModel): self._Output_kp_adists = OutputKpAdistsList(status=self._status) self._Pollutants = PollutantsList(status=self._status) self._InitialConditionsAdisTS = InitialConditionsAdisTSList(status=self._status) + self._BoundaryConditionsAdisTS = BoundaryConditionsAdisTSList(status=self._status) @classmethod def _db_create(cls, execute): @@ -343,6 +346,8 @@ class River(Graph, SQLSubModel): new._InitialConditionsAdisTS = InitialConditionsAdisTSList._db_load(execute, data) + new._BoundaryConditionsAdisTS = BoundaryConditionsAdisTSList._db_load(execute, data) + return new def _db_save(self, execute, data=None): @@ -365,6 +370,7 @@ class River(Graph, SQLSubModel): objs.append(self._Output_kp_adists) objs.append(self._Pollutants) objs.append(self._InitialConditionsAdisTS) + objs.append(self._BoundaryConditionsAdisTS) self._save_submodel(execute, objs, data) return True @@ -500,6 +506,10 @@ Last export at: @date.""" def initial_conditions_adists(self): return self._InitialConditionsAdisTS + @property + def boundary_conditions_adists(self): + return self._BoundaryConditionsAdisTS + def get_params(self, solver): if solver in self._parameters: return self._parameters[solver] diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Plot.py b/src/View/BoundaryConditionsAdisTS/Edit/Plot.py new file mode 100644 index 0000000000000000000000000000000000000000..5405a501315a74c96839b2c6c27b8ce81355a792 --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/Edit/Plot.py @@ -0,0 +1,104 @@ +# Plot.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 datetime import datetime + +from tools import timer, trace +from View.Tools.PamhyrPlot import PamhyrPlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class Plot(PamhyrPlot): + def __init__(self, mode="time", data=None, + trad=None, canvas=None, toolbar=None, + parent=None): + super(Plot, self).__init__( + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent + ) + + self._table_headers = self._trad.get_dict("table_headers") + + header = self.data.header + self.label_x = self._table_headers[header[0]] + self.label_y = self._table_headers[header[1]] + + self._mode = mode + self._isometric_axis = False + + self._auto_relim_update = True + self._autoscale_update = True + + def custom_ticks(self): + if self.data.header[0] != "time": + return + + self.set_ticks_time_formater() + + @timer + def draw(self): + self.init_axes() + + if len(self.data) == 0: + self._init = False + return + + self.draw_data() + self.custom_ticks() + + self.idle() + self._init = True + + def draw_data(self): + # Plot data + x = list(map(lambda v: v[0], self.data.data)) + y = list(map(lambda v: v[1], self.data.data)) + + self._line, = self.canvas.axes.plot( + x, y, + color=self.color_plot, + **self.plot_default_kargs + ) + + @timer + def update(self, ind=None): + if not self._init: + self.draw() + return + + self.update_data() + + self.update_idle() + + def update_data(self): + x = list(map(lambda v: v[0], self.data.data)) + y = list(map(lambda v: v[1], self.data.data)) + + self._line.set_data(x, y) diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Table.py b/src/View/BoundaryConditionsAdisTS/Edit/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..287a8fe42608c186849a2ab028dd139b56ce90ac --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/Edit/Table.py @@ -0,0 +1,193 @@ +# 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 datetime import date, time, datetime, timedelta + +from tools import ( + trace, timer, + timestamp_to_old_pamhyr_date, + old_pamhyr_date_to_timestamp +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, QTime, QDateTime, +) + +from PyQt5.QtWidgets import ( + QTableView, QAbstractItemView, QSpinBox, + QTimeEdit, QDateTimeEdit, QItemDelegate, +) + +from Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, PonctualContribution, + TimeOverZ, TimeOverDischarge, ZOverDischarge +) + +from View.BoundaryCondition.Edit.UndoCommand import ( + SetDataCommand, AddCommand, DelCommand, + SortCommand, MoveCommand, PasteCommand, +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class TableModel(PamhyrTableModel): + def data(self, index, role): + if role == Qt.TextAlignmentRole: + return Qt.AlignHCenter | Qt.AlignVCenter + + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + value = QVariant() + + if 0 <= column < 2: + v = self._data.get_i(row)[column] + if self._data.get_type_column(column) == float: + value = f"{v:.4f}" + elif self._data.header[column] == "time": + if self._opt_data == "time": + value = timestamp_to_old_pamhyr_date(int(v)) + else: + value = str(datetime.fromtimestamp(v)) + else: + value = f"{v}" + + return value + + 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: + self._undo.push( + SetDataCommand( + self._data, row, 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._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() + + def sort(self, _reverse, parent=QModelIndex()): + self.layoutAboutToBeChanged.emit() + + self._undo.push( + SortCommand( + self._data, _reverse + ) + ) + + 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_stack.push( + MoveCommand( + self._data, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._data): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._data, "down", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def paste(self, row, header, data): + if len(data) == 0: + return + + self.layoutAboutToBeChanged.emit() + + self._undo.push( + PasteCommand( + self._data, row, + list( + map( + lambda d: self._data.new_from_data(header, d), + data + ) + ) + ) + ) + + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() diff --git a/src/View/BoundaryConditionsAdisTS/Edit/UndoCommand.py b/src/View/BoundaryConditionsAdisTS/Edit/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..02e0a48ed0ed63f6a2bf832317b9291cc49cee11 --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/Edit/UndoCommand.py @@ -0,0 +1,183 @@ +# 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, +) + +from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition + +logger = logging.getLogger() + + +class SetDataCommand(QUndoCommand): + def __init__(self, data, index, column, new_value): + QUndoCommand.__init__(self) + + self._data = data + self._index = index + self._column = column + self._old = self._data.get_i(self._index)[self._column] + _type = self._data.get_type_column(self._column) + self._new = _type(new_value) + + def undo(self): + self._data._set_i_c_v(self._index, self._column, self._old) + + def redo(self): + self._data._set_i_c_v(self._index, self._column, self._new) + + +class SetMetaDataCommand(QUndoCommand): + def __init__(self, data, column, new_value): + QUndoCommand.__init__(self) + + self._data = data + self._column = column + if self._column == "d50": + self._old = self._data.d50 + elif self._column == "sigma": + self._old = self._data.sigma + + self._new = float(new_value) + + def undo(self): + if self._column == "d50": + self._data.d50 = self._old + elif self._column == "sigma": + self._data.sigma = self._old + + def redo(self): + if self._column == "d50": + self._data.d50 = self._new + elif self._column == "sigma": + self._data.sigma = self._new + + +class AddCommand(QUndoCommand): + def __init__(self, data, index): + QUndoCommand.__init__(self) + + self._data = data + self._index = index + self._new = None + + def undo(self): + self._data.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._data.add(self._index) + else: + self._data.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, data, rows): + QUndoCommand.__init__(self) + + self._data = data + self._rows = rows + + self._bc = [] + for row in rows: + self._bc.append((row, self._data.get_i(row))) + self._bc.sort() + + def undo(self): + for row, el in self._bc: + self._data.insert(row, el) + + def redo(self): + self._data.delete_i(self._rows) + + +class SortCommand(QUndoCommand): + def __init__(self, data, _reverse): + QUndoCommand.__init__(self) + + self._data = data + self._reverse = _reverse + + self._old = self._data.data + self._indexes = None + + def undo(self): + ll = self._data.data + self._data.sort( + key=lambda x: self._indexes[ll.index(x)] + ) + + def redo(self): + self._data.sort( + _reverse=self._reverse, + key=lambda x: x[0] + ) + if self._indexes is None: + self._indexes = list( + map( + lambda p: self._old.index(p), + self._data.data + ) + ) + self._old = None + + +class MoveCommand(QUndoCommand): + def __init__(self, data, up, i): + QUndoCommand.__init__(self) + + self._data = data + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._data.move_up(self._i) + else: + self._data.move_down(self._i) + + def redo(self): + if self._up: + self._data.move_up(self._i) + else: + self._data.move_down(self._i) + + +class PasteCommand(QUndoCommand): + def __init__(self, data, row, bcs): + QUndoCommand.__init__(self) + + self._data = data + self._row = row + self._bcs = bcs + self._bcs.reverse() + + def undo(self): + self._data.delete_i( + range(self._row, self._row + len(self._bcs)) + ) + + def redo(self): + for bc in self._bcs: + self._data.insert(self._row, bc) diff --git a/src/View/BoundaryConditionsAdisTS/Edit/Window.py b/src/View/BoundaryConditionsAdisTS/Edit/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..faf418964eb3dcca530f2c51eafbfd0a893359fd --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/Edit/Window.py @@ -0,0 +1,312 @@ +# 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 View.Tools.PamhyrWidget import PamhyrWidget +from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.BoundaryCondition.Edit.translate import BCETranslate +from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand +from View.BoundaryCondition.Edit.Table import TableModel +from View.BoundaryCondition.Edit.Plot import Plot + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class WD50Sigma(PamhyrWidget): + _pamhyr_ui = "d50sigma" + + d50Changed = pyqtSignal(float) + sigmaChanged = pyqtSignal(float) + + def __init__(self, parent=None): + super(WD50Sigma, self).__init__( + parent=parent + ) + + self.spinBox_d50 = self.find(QDoubleSpinBox, "doubleSpinBox_d50") + self.spinBox_sigma = self.find(QDoubleSpinBox, "doubleSpinBox_sigma") + + self.spinBox_d50.valueChanged.connect(self.valueChangedD50) + self.spinBox_sigma.valueChanged.connect(self.valueChangedSigma) + + def set_d50(self, d50): + self.spinBox_d50.valueChanged.disconnect(self.valueChangedD50) + self.spinBox_d50.setValue(float(d50)) + self.spinBox_d50.valueChanged.connect(self.valueChangedD50) + + def get_d50(self): + return float(self.spinBox_d50.value()) + + def set_sigma(self, sigma): + self.spinBox_sigma.valueChanged.disconnect(self.valueChangedSigma) + self.spinBox_sigma.setValue(float(sigma)) + self.spinBox_sigma.valueChanged.connect(self.valueChangedSigma) + + def get_sigma(self): + return float(self.spinBox_sigma.value()) + + @QtCore.pyqtSlot(float) + def valueChangedD50(self, value): + self.d50Changed.emit(value) + + @QtCore.pyqtSlot(float) + def valueChangedSigma(self, value): + self.sigmaChanged.emit(value) + + +class EditBoundaryConditionWindow(PamhyrWindow): + _pamhyr_ui = "EditBoundaryConditions" + _pamhyr_name = "Edit Boundary Conditions" + + def __init__(self, data=None, study=None, config=None, parent=None): + self._data = data + trad = BCETranslate() + self._long_types = trad.get_dict("long_types") + + name = trad[self._pamhyr_name] + if self._data is not None: + node_name = (self._data.node.name if self._data.node is not None + else trad['not_associated']) + name += ( + f" - {study.name} " + + f" - {self._data.name} ({self._data.id}) " + + f"({self._long_types[self._data.bctype]} - {node_name})" + ) + + super(EditBoundaryConditionWindow, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + self._hash_data.append(data) + + self.setup_table() + self.setup_plot() + self.setup_data() + self.setup_connections() + + def setup_data(self): + self._is_solid = self._data.bctype == "SL" + + if self._is_solid: + layout = self.find(QVBoxLayout, "verticalLayout_table") + self._d50sigma = WD50Sigma(parent=self) + layout.addWidget(self._d50sigma) + + self._d50sigma.set_d50(self._data.d50) + self._d50sigma.set_sigma(self._data.sigma) + + def setup_table(self): + headers = {} + table_headers = self._trad.get_dict("table_headers") + for h in self._data.header: + headers[h] = table_headers[h] + + self._delegate_time = PamhyrExTimeDelegate( + data=self._data, + mode=self._study.time_system, + parent=self + ) + + table = self.find(QTableView, "tableView") + self._table = TableModel( + table_view=table, + table_headers=headers, + editable_headers=self._data.header, + delegates={ + # "time": self._delegate_time, + }, + data=self._data, + undo=self._undo_stack, + opt_data=self._study.time_system + ) + + table.setModel(self._table) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_plot(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.verticalLayout.addWidget(self.toolbar) + self.verticalLayout.addWidget(self.canvas) + + self.plot = Plot( + canvas=self.canvas, + data=self._data, + mode=self._study.time_system, + trad=self._trad, + toolbar=self.toolbar, + ) + self.plot.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._table.dataChanged.connect(self.update) + + if self._is_solid: + self._d50sigma.d50Changed.connect(self.d50_changed) + self._d50sigma.sigmaChanged.connect(self.sigma_changed) + + def d50_changed(self, value): + self._undo_stack.push( + SetMetaDataCommand( + self._data, + "d50", value + ) + ) + + def sigma_changed(self, value): + self._undo_stack.push( + SetMetaDataCommand( + self._data, + "sigma", value + ) + ) + + def widget_update(self): + if self._is_solid: + self._d50sigma.set_d50(self._data.d50) + self._d50sigma.set_sigma(self._data.sigma) + + def update(self): + self.plot.update() + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + return table.selectionModel()\ + .selectedRows()[0]\ + .row() + + 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._data) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + self.plot.update() + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + self.plot.update() + + def sort(self): + self._table.sort(False) + self.plot.update() + + def move_up(self): + row = self.index_selected_row() + self._table.move_up(row) + self.plot.update() + + def move_down(self): + row = self.index_selected_row() + self._table.move_down(row) + self.plot.update() + + def _copy(self): + rows = self.index_selected_rows() + + table = [] + table.append(self._data.header) + + data = self._data.data + for row in rows: + table.append(list(data[row])) + + self.copyTableIntoClipboard(table) + + def _paste(self): + header, data = self.parseClipboardTable() + + logger.debug(f"paste: h:{header}, d:{data}") + + if len(data) == 0: + return + + row = 0 + rows = self.index_selected_rows() + if len(rows) != 0: + row = rows[0] + + self._table.paste(row, header, data) + self.plot.update() + + def _undo(self): + self._table.undo() + self.plot.update() + self.widget_update() + + def _redo(self): + self._table.redo() + self.plot.update() + self.widget_update() diff --git a/src/View/BoundaryConditionsAdisTS/Edit/translate.py b/src/View/BoundaryConditionsAdisTS/Edit/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..9d8355505cf0477acafa811fe48d16f33b04f695 --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/Edit/translate.py @@ -0,0 +1,44 @@ +# 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 + +from View.BoundaryCondition.translate import BCTranslate + +_translate = QCoreApplication.translate + + +class BCETranslate(BCTranslate): + def __init__(self): + super(BCETranslate, self).__init__() + + self._dict["Edit Boundary Conditions"] = _translate( + "BoundaryCondition", "Edit boundary conditions" + ) + + self._sub_dict["table_headers"] = { + "x": _translate("BoundaryCondition", "X"), + "y": _translate("BoundaryCondition", "Y"), + "time": self._dict["time"], + "date": self._dict["date"], + "discharge": self._dict["unit_discharge"], + "z": self._dict["unit_elevation"], + "solid": _translate("BoundaryCondition", "Solid (kg/s)"), + } diff --git a/src/View/BoundaryConditionsAdisTS/Table.py b/src/View/BoundaryConditionsAdisTS/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..eec695cbadc8d81a134d01b4052a7b671b055b05 --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/Table.py @@ -0,0 +1,264 @@ +# 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 Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, PonctualContribution, + TimeOverZ, TimeOverDischarge, ZOverDischarge +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.BoundaryCondition.UndoCommand import ( + SetNameCommand, SetNodeCommand, SetTypeCommand, + AddCommand, DelCommand, SortCommand, + MoveCommand, PasteCommand, +) +from View.BoundaryCondition.translate import BC_types + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, mode="type", tab="", + trad=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._mode = mode + self._tab = tab + 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) + + if self._mode == "type": + lst = list( + map( + lambda k: self._long_types[k], + filter( + lambda k: self._tab in BC_types[k].compatibility(), + BC_types.keys() + ) + ) + ) + self.editor.addItems( + lst + ) + else: + self.editor.addItems( + [self._trad["not_associated"]] + + self._data.nodes_names() + ) + + 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 _setup_lst(self): + self._lst = self._data.boundary_condition + self._tab = self._opt_data + + def rowCount(self, parent): + return self._lst.len(self._tab) + + 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(self._tab, row).name + elif self._headers[column] == "type": + t = self._lst.get(self._tab, row).bctype + return self._long_types[t] + elif self._headers[column] == "node": + n = self._lst.get(self._tab, row).node + if n is None: + return self._trad["not_associated"] + return n.name + + 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._lst, self._tab, row, value + ) + ) + elif self._headers[column] == "type": + key = next(k for k, v in self._long_types.items() + if v == value) + self._undo.push( + SetTypeCommand( + self._lst, self._tab, row, BC_types[key] + ) + ) + elif self._headers[column] == "node": + self._undo.push( + SetNodeCommand( + self._lst, self._tab, row, self._data.node(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, self._tab, 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, self._tab, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def sort(self, _reverse, parent=QModelIndex()): + self.layoutAboutToBeChanged.emit() + + self._undo.push( + SortCommand( + self._lst, self._tab, 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_stack.push( + MoveCommand( + self._lst, self._tab, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._lst): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._lst, self._tab, "down", row + ) + ) + + self.endMoveRows() + 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/BoundaryConditionsAdisTS/UndoCommand.py b/src/View/BoundaryConditionsAdisTS/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..ee3b45df49fd99a6f7e947ade7a7f5fabae8c326 --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/UndoCommand.py @@ -0,0 +1,204 @@ +# 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.BoundaryCondition.BoundaryCondition import BoundaryCondition +from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList + + +class SetNameCommand(QUndoCommand): + def __init__(self, bcs, tab, index, new_value): + QUndoCommand.__init__(self) + + self._bcs = bcs + self._tab = tab + self._index = index + self._old = self._bcs.get(self._tab, self._index).name + self._new = str(new_value) + + def undo(self): + self._bcs.get(self._tab, self._index).name = self._old + + def redo(self): + self._bcs.get(self._tab, self._index).name = self._new + + +class SetNodeCommand(QUndoCommand): + def __init__(self, bcs, tab, index, node): + QUndoCommand.__init__(self) + + self._bcs = bcs + self._tab = tab + self._index = index + self._old = self._bcs.get(self._tab, self._index).node + self._new = node + self._prev_assoc_to_node = self._bcs.get_assoc_to_node(tab, node) + + def _previous_assoc_node(self, node): + if self._prev_assoc_to_node is not None: + self._prev_assoc_to_node.node = node + + def undo(self): + self._bcs.get(self._tab, self._index).node = self._old + self._previous_assoc_node(self._new) + + def redo(self): + self._bcs.get(self._tab, self._index).node = self._new + self._previous_assoc_node(None) + + +class SetTypeCommand(QUndoCommand): + def __init__(self, bcs, tab, index, _type): + QUndoCommand.__init__(self) + + self._bcs = bcs + self._tab = tab + self._index = index + self._type = _type + self._old = self._bcs.get(self._tab, self._index) + self._new = self._bcs.get(self._tab, self._index)\ + .convert(self._type) + + def undo(self): + self._bcs.set(self._tab, self._index, self._old) + + def redo(self): + self._bcs.set(self._tab, self._index, self._new) + + +class AddCommand(QUndoCommand): + def __init__(self, bcs, tab, index): + QUndoCommand.__init__(self) + + self._bcs = bcs + self._tab = tab + self._index = index + self._new = None + + def undo(self): + self._bcs.delete_i(self._tab, [self._index]) + + def redo(self): + if self._new is None: + self._new = self._bcs.new(self._tab, self._index) + else: + self._bcs.insert(self._tab, self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, bcs, tab, rows): + QUndoCommand.__init__(self) + + self._bcs = bcs + self._tab = tab + self._rows = rows + + self._bc = [] + for row in rows: + self._bc.append((row, self._bcs.get(self._tab, row))) + self._bc.sort() + + def undo(self): + for row, el in self._bc: + self._bcs.insert(self._tab, row, el) + + def redo(self): + self._bcs.delete_i(self._tab, self._rows) + + +class SortCommand(QUndoCommand): + def __init__(self, bcs, tab, _reverse): + QUndoCommand.__init__(self) + + self._bcs = bcs + self._tab = tab + self._reverse = _reverse + + self._old = self._bcs.get_tab(self._tab) + self._indexes = None + + def undo(self): + ll = self._bcs.get_tab(self._tab) + self._bcs.sort( + self._tab, + key=lambda x: self._indexes[ll.index(x)] + ) + + def redo(self): + self._bcs.sort( + self._tab, + reverse=self._reverse, + key=lambda x: x.name + ) + if self._indexes is None: + self._indexes = list( + map( + lambda p: self._old.index(p), + self._bcs.get_tab(self._tab) + ) + ) + self._old = None + + +class MoveCommand(QUndoCommand): + def __init__(self, bcs, tab, up, i): + QUndoCommand.__init__(self) + + self._bcs = bcs + self._tab = tab + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._bcs.move_up(self._tab, self._i) + else: + self._bcs.move_down(self._tab, self._i) + + def redo(self): + if self._up: + self._bcs.move_up(self._tab, self._i) + else: + self._bcs.move_down(self._tab, self._i) + + +class PasteCommand(QUndoCommand): + def __init__(self, bcs, tab, row, bc): + QUndoCommand.__init__(self) + + self._bcs = bcs + self._tab = tab + self._row = row + self._bc = deepcopy(bc) + self._bc.reverse() + + def undo(self): + self._bcs.delete_i( + self._tab, + range(self._row, self._row + len(self._bc)) + ) + + def redo(self): + for bc in self._bc: + self._bcs.insert(self._tab, self._row, bc) diff --git a/src/View/BoundaryConditionsAdisTS/Window.py b/src/View/BoundaryConditionsAdisTS/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..5aae8ffb4824b157e9626306bd5f815bb049126c --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/Window.py @@ -0,0 +1,230 @@ +# 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 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, + QWidget, +) + +from Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, PonctualContribution, + TimeOverZ, TimeOverDischarge, ZOverDischarge +) + +from View.BoundaryCondition.UndoCommand import ( + SetNameCommand, SetNodeCommand, SetTypeCommand, + AddCommand, DelCommand, SortCommand, + MoveCommand, PasteCommand, +) + +from View.BoundaryConditionsAdisTS.Table import ( + TableModel, ComboBoxDelegate +) + +from View.Network.GraphWidget import GraphWidget +from View.BoundaryConditionsAdisTS.translate import BCAdisTSTranslate +from View.BoundaryConditionsAdisTS.Edit.Window import EditBoundaryConditionWindow + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class BoundaryConditionWindow(PamhyrWindow): + _pamhyr_ui = "BoundaryConditionsAdisTS" + _pamhyr_name = "Boundary conditions AdisTS" + + def __init__(self, study=None, config=None, parent=None): + trad = BCAdisTSTranslate() + name = ( + trad[self._pamhyr_name] + + " - " + study.name + ) + + super(BoundaryConditionWindow, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + self._bcs = self._study.river.boundary_condition + + self.setup_table() + self.setup_graph() + self.setup_connections() + + self.ui.setWindowTitle(self._title) + + def setup_table(self): + self._table = {} + + t = "liquid" + + self._delegate_type = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + mode="type", + tab=t, + parent=self + ) + self._delegate_node = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + mode="node", + tab=t, + parent=self + ) + + table = self.find(QTableView, f"tableView_{t}") + self._table[t] = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "type", "node"], + delegates={ + "type": self._delegate_type, + "node": self._delegate_node, + }, + trad=self._trad, + data=self._study.river, + undo=self._undo_stack, + opt_data=t, + ) + table.setModel(self._table[t]) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_graph(self): + self.graph_widget = GraphWidget( + self._study.river, + min_size=None, size=(200, 200), + only_display=True, + parent=self + ) + self.graph_layout = self.find(QVBoxLayout, "verticalLayout") + self.graph_layout.addWidget(self.graph_widget) + + 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_edit").triggered.connect(self.edit) + self.find(QAction, "action_sort").triggered.connect(self.sort) + + def index_selected_row(self): + tab = "liquid" + table = self.find(QTableView, f"tableView_{tab}") + return table.selectionModel()\ + .selectedRows()[0]\ + .row() + + def index_selected_rows(self): + tab = "liquid" + table = self.find(QTableView, f"tableView_{tab}") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + tab = "liquid" + rows = self.index_selected_rows() + if self._bcs.len(tab) == 0 or len(rows) == 0: + self._table[tab].add(0) + else: + self._table[tab].add(rows[0]) + + def delete(self): + tab = "liquid" + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table[tab].delete(rows) + + def sort(self): + tab = "liquid" + self._table[tab].sort(False) + + def move_up(self): + tab = "liquid" + row = self.index_selected_row() + self._table[tab].move_up(row) + + def move_down(self): + tab = "liquid" + row = self.index_selected_row() + self._table[tab].move_down(row) + + def _copy(self): + logger.info("TODO: copy") + + def _paste(self): + logger.info("TODO: paste") + + def _undo(self): + tab = "liquid" + self._table[tab].undo() + + def _redo(self): + tab = "liquid" + self._table[tab].redo() + + def edit(self): + tab = "liquid" + rows = self.index_selected_rows() + for row in rows: + data = self._bcs.get(tab, row) + + if self.sub_window_exists( + EditBoundaryConditionWindow, + data=[self._study, None, data] + ): + continue + + win = EditBoundaryConditionWindow( + data=data, + study=self._study, + parent=self + ) + win.show() diff --git a/src/View/BoundaryConditionsAdisTS/translate.py b/src/View/BoundaryConditionsAdisTS/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..967bea50728056b1f0103e1edc65bbc0b4eb7c26 --- /dev/null +++ b/src/View/BoundaryConditionsAdisTS/translate.py @@ -0,0 +1,46 @@ +# 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 BCAdisTSTranslate(MainTranslate): + def __init__(self): + super(BCAdisTSTranslate, self).__init__() + + self._dict["Boundary conditions AdisTS"] = _translate( + "BoundaryConditionsAdisTS", "Boundary conditions AdisTS" + ) + + self._sub_dict["long_types"] = { + "ND": self._dict["not_defined"], + "PC": _translate("BoundaryCondition", "Ponctual contribution"), + "TZ": _translate("BoundaryCondition", "Z(t)"), + "TD": _translate("BoundaryCondition", "Q(t)"), + "ZD": _translate("BoundaryCondition", "Q(Z)"), + "SL": _translate("BoundaryCondition", "Solid"), + } + + self._sub_dict["table_headers"] = { + "name": self._dict["name"], + "type": self._dict["type"], + "node": _translate("BoundaryCondition", "Node") + } diff --git a/src/View/Pollutants/Window.py b/src/View/Pollutants/Window.py index 93b091474dfcb411655d67b22d713af58599f473..0914d045df969739de39d9c51c0673a9487853e5 100644 --- a/src/View/Pollutants/Window.py +++ b/src/View/Pollutants/Window.py @@ -44,6 +44,7 @@ from View.Pollutants.Translate import PollutantsTranslate from View.Pollutants.Edit.Window import EditPolluantWindow from View.InitialConditionsAdisTS.Window import InitialConditionsAdisTSWindow +from View.BoundaryConditionsAdisTS.Window import BoundaryConditionWindow logger = logging.getLogger() @@ -105,6 +106,7 @@ class PollutantsWindow(PamhyrWindow): 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.find(QAction, "action_boundary_conditions").triggered.connect(self.boundary_conditions) self._checkbox.clicked.connect(self._set_structure_state) table = self.find(QTableView, "tableView") @@ -211,6 +213,20 @@ class PollutantsWindow(PamhyrWindow): ) initial.show() + def boundary_conditions(self, tab=0): + if self.sub_window_exists( + BoundaryConditionWindow, + data=[self._study, None] + ): + bound = self.get_sub_window( + BoundaryConditionWindow, + data=[self._study, None] + ) + return + + bound = BoundaryConditionWindow(study=self._study, parent=self) + bound.show() + def _set_checkbox_state(self): row = self.index_selected_row() if row is None: diff --git a/src/View/ui/BoundaryConditionsAdisTS.ui b/src/View/ui/BoundaryConditionsAdisTS.ui new file mode 100644 index 0000000000000000000000000000000000000000..b54d77f62fa91ada02de922c684836a714692298 --- /dev/null +++ b/src/View/ui/BoundaryConditionsAdisTS.ui @@ -0,0 +1,148 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>450</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <property name="locale"> + <locale language="English" country="Europe"/> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QTabWidget" name="tabWidget"> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab_liquid"> + <attribute name="title"> + <string>Liquid</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QTableView" name="tableView_liquid"/> + </item> + </layout> + </widget> + </widget> + <widget class="QWidget" name="verticalLayoutWidget"> + <layout class="QVBoxLayout" name="verticalLayout"/> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>22</height> + </rect> + </property> + </widget> + <widget class="QToolBar" name="toolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="action_add"/> + <addaction name="action_del"/> + <addaction name="action_edit"/> + <addaction name="action_sort"/> + </widget> + <action name="action_add"> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="icon"> + <iconset> + <normaloff>ressources/add.png</normaloff>ressources/add.png</iconset> + </property> + <property name="text"> + <string>Add</string> + </property> + <property name="toolTip"> + <string>Add a new boundary condition or punctual contribution</string> + </property> + <property name="shortcut"> + <string>Ctrl+N</string> + </property> + </action> + <action name="action_del"> + <property name="icon"> + <iconset> + <normaloff>ressources/del.png</normaloff>ressources/del.png</iconset> + </property> + <property name="text"> + <string>Delete</string> + </property> + <property name="toolTip"> + <string>Delete current selected rows</string> + </property> + <property name="shortcut"> + <string>Ctrl+D</string> + </property> + </action> + <action name="action_edit"> + <property name="icon"> + <iconset> + <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset> + </property> + <property name="text"> + <string>Edit</string> + </property> + <property name="toolTip"> + <string>Edit boundary condition or punctual contribution</string> + </property> + <property name="shortcut"> + <string>Ctrl+E</string> + </property> + </action> + <action name="action_sort"> + <property name="icon"> + <iconset> + <normaloff>ressources/sort_A-Z.png</normaloff>ressources/sort_A-Z.png</iconset> + </property> + <property name="text"> + <string>Sort</string> + </property> + <property name="toolTip"> + <string>Sort boundary condition by name</string> + </property> + </action> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/View/ui/Pollutants.ui b/src/View/ui/Pollutants.ui index 9ed80616a81d154a69e8e93c6b283bf719f8c505..3f7a288f9839b0fd24a9d661507f1ce1b8372d0a 100644 --- a/src/View/ui/Pollutants.ui +++ b/src/View/ui/Pollutants.ui @@ -97,6 +97,7 @@ <addaction name="action_delete"/> <addaction name="action_edit"/> <addaction name="action_initial_conditions"/> + <addaction name="action_boundary_conditions"/> </widget> <action name="action_add"> <property name="icon"> @@ -135,6 +136,11 @@ <string>InitialConditions</string> </property> </action> + <action name="action_boundary_conditions"> + <property name="text"> + <string>BoundaryConditions</string> + </property> + </action> </widget> <resources/> <connections/>