diff --git a/src/Model/DIFAdisTS/DIFAdisTS.py b/src/Model/DIFAdisTS/DIFAdisTS.py new file mode 100644 index 0000000000000000000000000000000000000000..c04479416e45e7d53b5933b164db4306e8847a4e --- /dev/null +++ b/src/Model/DIFAdisTS/DIFAdisTS.py @@ -0,0 +1,258 @@ +# DIFAdisTS.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 functools import reduce + +from tools import trace, timer, old_pamhyr_date_to_timestamp + +from Model.Tools.PamhyrDB import SQLSubModel +from Model.Except import NotImplementedMethodeError + +from Model.DIFAdisTS.DIFAdisTSSpec import DIFAdisTSSpec + +logger = logging.getLogger() + +class DIFAdisTS(SQLSubModel): + _sub_classes = [ + DIFAdisTSSpec, + ] + _id_cnt = 0 + + def __init__(self, id: int = -1, name: str = "default", + status=None): + super(DIFAdisTS, self).__init__() + + self._status = status + + if id == -1: + self.id = DIFAdisTS._id_cnt + else: + self.id = id + + self._name = name + self._method = None + self._dif = None + self._b = None + self._c = None + self._enabled = True + self._types = ["iwasa", "fisher", "elder", "constante", "generique"] + self._data = [] + + DIFAdisTS._id_cnt = max( + DIFAdisTS._id_cnt + 1, + self.id + ) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE dif_adists( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + method TEXT NOT NULL, + dif REAL NOT NULL, + b REAL, + c REAL, + enabled BOOLEAN NOT NULL + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 6: + cls._db_create(execute) + + return True + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + + table = execute( + "SELECT id, name, method, dif, b, c, enabled " + + "FROM dif_adists" + ) + + if table is not None: + for row in table: + dif_id = row[0] + name = row[1] + method = row[2] + dif = row[3] + b = row[4] + c = row[5] + enabled = (row[6] == 1) + + DIF = cls( + id=dif_id, + name=name, + status=data['status'] + ) + + DIF.method = method + DIF.dif = dif + DIF.b = b + DIF.c = c + DIF.enabled = enabled + + data['dif_default_id'] = dif_id + DIF._data = DIFAdisTSSpec._db_load(execute, data) + + new.append(DIF) + + return new + + def _db_save(self, execute, data=None): + execute(f"DELETE FROM dif_adists WHERE id = {self.id}") + + method = "" + if self.method is not None: + method = self.method + + dif = -1. + if self.dif is not None: + dif = self.dif + + b = -1. + if self.b is not None: + b = self.b + + c = -1. + if self.dif is not None: + c = self.c + + sql = ( + "INSERT INTO " + + "dif_adists(" + + "id, name, method, dif, b, c, enabled" + + ") " + + "VALUES (" + + f"{self.id}, '{self._db_format(self._name)}', " + + f"'{self._db_format(self._method)}', " + + f"{dif}, {b}, {c}, {self._enabled}" + + ")" + ) + + execute(sql) + + data['dif_default_id'] = self.id + execute( + "DELETE FROM dif_spec " + + f"WHERE dif_default = {self.id}" + ) + + for dif_spec in self._data: + dif_spec._db_save(execute, data) + + return True + + def __len__(self): + return len(self._data) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + self._status.modified() + + @property + def method(self): + return self._method + + @method.setter + def method(self, method): + self._method = method + self._status.modified() + + @property + def types(self): + return self._types + + @property + def dif(self): + return self._dif + + @dif.setter + def dif(self, dif): + self._dif = dif + self._status.modified() + + @property + def b(self): + return self._b + + @b.setter + def b(self, b): + self._b = b + self._status.modified() + + @property + def c(self): + return self._c + + @c.setter + def c(self, c): + self._c = c + self._status.modified() + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, enabled): + self._enabled = enabled + self._status.modified() + + def new(self, index): + n = DIFAdisTSSpec(status=self._status) + self._data.insert(index, n) + self._status.modified() + return n + + def delete(self, data): + self._data = list( + filter( + lambda x: x not in data, + self._data + ) + ) + self._status.modified() + + def delete_i(self, indexes): + for ind in indexes: + del self._data[ind] + self._status.modified() + + def insert(self, index, data): + self._data.insert(index, data) + self._status.modified() + + + + + + diff --git a/src/Model/DIFAdisTS/DIFAdisTSList.py b/src/Model/DIFAdisTS/DIFAdisTSList.py new file mode 100644 index 0000000000000000000000000000000000000000..89eff085ff6b91721cf2efb93633449d8d7ac348 --- /dev/null +++ b/src/Model/DIFAdisTS/DIFAdisTSList.py @@ -0,0 +1,62 @@ +# DIFAdisTSList.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.DIFAdisTS.DIFAdisTS import DIFAdisTS + +class DIFAdisTSList(PamhyrModelList): + _sub_classes = [ + DIFAdisTS, + ] + + @classmethod + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) + + if data is None: + data = {} + + new._lst = DIFAdisTS._db_load( + execute, data + ) + + return new + + def _db_save(self, execute, data=None): + execute("DELETE FROM dif_adists") + + if data is None: + data = {} + + for dif in self._lst: + dif._db_save(execute, data=data) + + return True + + def new(self, index): + n = DIFAdisTS(status=self._status) + self._lst.insert(index, n) + self._status.modified() + return n + + + + diff --git a/src/Model/DIFAdisTS/DIFAdisTSSpec.py b/src/Model/DIFAdisTS/DIFAdisTSSpec.py new file mode 100644 index 0000000000000000000000000000000000000000..6850fb6559776141b270275096e74916d997fb10 --- /dev/null +++ b/src/Model/DIFAdisTS/DIFAdisTSSpec.py @@ -0,0 +1,228 @@ +# DIFAdisTSSpec.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 + +from Model.Tools.PamhyrDB import SQLSubModel +from Model.Except import NotImplementedMethodeError + +logger = logging.getLogger() + +class DIFAdisTSSpec(SQLSubModel): + _sub_classes = [ + ] + _id_cnt = 0 + + def __init__(self, id: int = -1, method: str = "", + status=None): + super(DIFAdisTSSpec, self).__init__() + + self._status = status + + if id == -1: + self.id = DIFAdisTSSpec._id_cnt + else: + self.id = id + + self._method = method + self._reach = None + self._start_kp = None + self._end_kp = None + self._dif = None + self._b = None + self._c = None + self._enabled = True + + DIFAdisTSSpec._id_cnt = max(DIFAdisTSSpec._id_cnt + 1, self.id) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE dif_spec( + id INTEGER NOT NULL PRIMARY KEY, + dif_default INTEGER NOT NULL, + method TEXT NOT NULL, + reach INTEGER NOT NULL, + start_kp REAL NOT NULL, + end_kp REAL NOT NULL, + dif REAL NOT NULL, + b REAL, + c REAL, + enabled BOOLEAN NOT NULL, + FOREIGN KEY(dif_default) REFERENCES dif_adists(id), + FOREIGN KEY(reach) REFERENCES river_reach(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 6: + cls._db_create(execute) + + return True + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + + table = execute( + "SELECT id, dif_default, method, reach, start_kp, end_kp, " + + "dif, b, c, enabled " + + "FROM dif_spec " + + f"WHERE dif_default = {data['dif_default_id']} " + ) + + for row in table: + id = row[0] + method = row[2] + reach = row[3] + start_kp = row[4] + end_kp = row[5] + dif = row[6] + b = row[7] + c = row[8] + enabled = (row[9] == 1) + + new_spec = cls( + id=id, + method=method, + status=data['status'] + ) + + new_spec.reach = reach + new_spec.start_kp = start_kp + new_spec.end_kp = end_kp + new_spec.dif = dif + new_spec.b = b + new_spec.c = c + new_spec.enabled = enabled + + new.append(new_spec) + + return new + + def _db_save(self, execute, data=None): + dif_default = data['dif_default_id'] + + sql = ( + "INSERT INTO " + + "dif_spec(id, dif_default, method, reach, " + + "start_kp, end_kp, dif, b, c, enabled) " + + "VALUES (" + + f"{self.id}, " + + f"{dif_default}, " + + f"'{self._db_format(self._method)}', " + + f"{self._reach}, " + + f"{self._start_kp}, " + + f"{self._end_kp}, " + + f"{self._dif}, " + + f"{self._b}, " + + f"{self._c}, " + + f"{self._enabled}" + + ")" + ) + execute(sql) + + return True + + @property + def method(self): + return self._method + + @method.setter + def method(self, method): + self._method = method + self._status.modified() + + @property + def reach(self): + return self._reach + + @reach.setter + def reach(self, reach): + self._reach = reach + self._status.modified() + + @property + def start_kp(self): + return self._start_kp + + @start_kp.setter + def start_kp(self, start_kp): + self._start_kp = start_kp + self._status.modified() + + @property + def end_kp(self): + return self._end_kp + + @end_kp.setter + def end_kp(self, end_kp): + self._end_kp = end_kp + self._status.modified() + + @property + def dif(self): + return self._dif + + @dif.setter + def dif(self, dif): + self._dif = dif + self._status.modified() + + @property + def b(self): + return self._b + + @b.setter + def b(self, b): + self._b = b + self._status.modified() + + @property + def c(self): + return self._c + + @c.setter + def c(self, c): + self._c = c + self._status.modified() + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, enabled): + self._enabled = enabled + self._status.modified() + + + + + + + + + diff --git a/src/Model/River.py b/src/Model/River.py index 4cc2c5e44b1f3b6001af16943e3f8100e3e79d14..e5797961c9f6188cda9c70a254548b9cf6f00ea9 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -51,6 +51,7 @@ from Model.InitialConditionsAdisTS.InitialConditionsAdisTSList import InitialCon from Model.BoundaryConditionsAdisTS.BoundaryConditionsAdisTSList import BoundaryConditionsAdisTSList from Model.LateralContributionsAdisTS.LateralContributionsAdisTSList import LateralContributionsAdisTSList from Model.D90AdisTS.D90AdisTSList import D90AdisTSList +from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList class RiverNode(Node, SQLSubModel): @@ -241,6 +242,7 @@ class River(Graph, SQLSubModel): BoundaryConditionsAdisTSList, LateralContributionsAdisTSList, D90AdisTSList, + DIFAdisTSList, ] def __init__(self, status=None): @@ -270,6 +272,7 @@ class River(Graph, SQLSubModel): self._BoundaryConditionsAdisTS = BoundaryConditionsAdisTSList(status=self._status) self._LateralContributionsAdisTS = LateralContributionsAdisTSList(status=self._status) self._D90AdisTS = D90AdisTSList(status=self._status) + self._DIFAdisTS = DIFAdisTSList(status=self._status) @classmethod def _db_create(cls, execute): @@ -358,6 +361,8 @@ class River(Graph, SQLSubModel): new._D90AdisTS = D90AdisTSList._db_load(execute, data) + new._DIFAdisTS = DIFAdisTSList._db_load(execute, data) + return new def _db_save(self, execute, data=None): @@ -383,6 +388,7 @@ class River(Graph, SQLSubModel): objs.append(self._BoundaryConditionsAdisTS) objs.append(self._LateralContributionsAdisTS) objs.append(self._D90AdisTS) + objs.append(self._DIFAdisTS) self._save_submodel(execute, objs, data) return True @@ -530,6 +536,10 @@ Last export at: @date.""" def d90_adists(self): return self._D90AdisTS + @property + def dif_adists(self): + return self._DIFAdisTS + def get_params(self, solver): if solver in self._parameters: return self._parameters[solver] diff --git a/src/View/DIFAdisTS/Table.py b/src/View/DIFAdisTS/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..13d5555cb7a43845c940398e56f6b7aded460514 --- /dev/null +++ b/src/View/DIFAdisTS/Table.py @@ -0,0 +1,217 @@ +# 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.D90AdisTS.UndoCommand import ( + SetCommand, AddCommand, SetCommandSpec, + DelCommand, +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, ic_spec_lst=None, trad=None, parent=None, mode="reaches"): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._mode = mode + self._trad = trad + self._ic_spec_lst = ic_spec_lst + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + val = [] + if self._mode == "kp": + reach_id = self._ic_spec_lst[index.row()].reach + + reach = next(filter(lambda edge: edge.id == reach_id, self._data.edges())) + + if reach_id 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 D90TableModel(PamhyrTableModel): + def __init__(self, river=None, data=None, **kwargs): + self._river = river + + super(D90TableModel, self).__init__(data=data, **kwargs) + + self._data = data + + def _setup_lst(self): + self._lst = self._data._data + + 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] is "name": + n = self._lst[row].name + if n is None or n == "": + return self._trad['not_associated'] + return n + elif self._headers[column] is "reach": + n = self._lst[row].reach + if n is None: + return self._trad['not_associated'] + return next(filter(lambda edge: edge.id == n, self._river.edges())).name + elif self._headers[column] is "start_kp": + n = self._lst[row].start_kp + if n is None: + return self._trad['not_associated'] + return n + elif self._headers[column] is "end_kp": + n = self._lst[row].end_kp + if n is None: + return self._trad['not_associated'] + return n + elif self._headers[column] is "d90": + n = self._lst[row].d90 + 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() + + try: + if self._headers[column] != "reach": + self._undo.push( + SetCommandSpec( + self._lst, row, self._headers[column], value + ) + ) + elif self._headers[column] == "reach": + print(self._river.edge(value).id) + self._undo.push( + SetCommandSpec( + self._lst, row, self._headers[column], self._river.edge(value).id + ) + ) + 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, 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._data, self._lst, rows + ) + ) + + self.endRemoveRows() + 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/DIFAdisTS/TableDefault.py b/src/View/DIFAdisTS/TableDefault.py new file mode 100644 index 0000000000000000000000000000000000000000..c3231d424916abef2ccb4c1790739737e44e5989 --- /dev/null +++ b/src/View/DIFAdisTS/TableDefault.py @@ -0,0 +1,95 @@ +# 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.D90AdisTS.UndoCommand import ( + SetCommand, +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + +class D90TableDefaultModel(PamhyrTableModel): + def __init__(self, **kwargs): + super(D90TableDefaultModel, self).__init__(**kwargs) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] is "name": + return self._data[row].name + elif self._headers[column] is "d90": + n = self._data[row].d90 + 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() + + try: + if self._headers[column] is not None: + self._undo.push( + SetCommand( + self._data, 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 undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() + diff --git a/src/View/DIFAdisTS/UndoCommand.py b/src/View/DIFAdisTS/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..f0644955f804a69300d441ff4787ac60a377ab7d --- /dev/null +++ b/src/View/DIFAdisTS/UndoCommand.py @@ -0,0 +1,150 @@ +# 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.D90AdisTS.D90AdisTS import D90AdisTS +from Model.D90AdisTS.D90AdisTSList import D90AdisTSList + + +class SetCommand(QUndoCommand): + def __init__(self, data, row, column, new_value): + QUndoCommand.__init__(self) + + self._data = data + self._row = row + self._column = column + + if self._column == "name": + self._old = self._data[self._row].name + elif self._column == "d90": + self._old = self._data[self._row].d90 + + _type = float + if column == "name": + _type = str + + self._new = _type(new_value) + + def undo(self): + if self._column == "name": + self._data[self._row].name = self._old + elif self._column == "d90": + self._data[self._row].d90 = self._old + + def redo(self): + if self._column == "name": + self._data[self._row].name = self._new + elif self._column == "d90": + self._data[self._row].d90 = self._new + +class SetCommandSpec(QUndoCommand): + def __init__(self, data, row, column, new_value): + QUndoCommand.__init__(self) + + self._data = data + self._row = row + self._column = column + + if self._column == "name": + self._old = self._data[self._row].name + elif self._column == "reach": + self._old = self._data[self._row].reach + elif self._column == "start_kp": + self._old = self._data[self._row].start_kp + elif self._column == "end_kp": + self._old = self._data[self._row].end_kp + elif self._column == "d90": + self._old = self._data[self._row].d90 + + _type = float + if column == "name": + _type = str + elif column == "reach": + _type = int + + self._new = _type(new_value) + + def undo(self): + if self._column == "name": + self._data[self._row].name = self._old + elif self._column == "reach": + self._data[self._row].reach = self._old + elif self._column == "start_kp": + self._data[self._row].start_kp = self._old + elif self._column == "end_kp": + self._data[self._row].end_kp = self._old + elif self._column == "d90": + self._data[self._row].d90 = self._old + + def redo(self): + if self._column == "name": + self._data[self._row].name = self._new + elif self._column == "reach": + self._data[self._row].reach = self._new + elif self._column == "start_kp": + self._data[self._row].start_kp = self._new + elif self._column == "end_kp": + self._data[self._row].end_kp = self._new + elif self._column == "d90": + self._data[self._row].d90 = self._new + +class AddCommand(QUndoCommand): + def __init__(self, data, ics_spec, index): + QUndoCommand.__init__(self) + + self._data = data + self._ics_spec = ics_spec + 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.new(self._index) + else: + self._data.insert(self._index, self._new) + +class DelCommand(QUndoCommand): + def __init__(self, data, ics_spec, rows): + QUndoCommand.__init__(self) + + self._data = data + self._ics_spec = ics_spec + self._rows = rows + #self._data = data + + self._ic = [] + for row in rows: + self._ic.append((row, self._ics_spec[row])) + self._ic.sort() + + def undo(self): + for row, el in self._ic: + self._data.insert(row, el) + + def redo(self): + self._data.delete_i(self._rows) + diff --git a/src/View/DIFAdisTS/Window.py b/src/View/DIFAdisTS/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..5fffc17d68a61d9e2134c39fe9996a4f3a33c576 --- /dev/null +++ b/src/View/DIFAdisTS/Window.py @@ -0,0 +1,283 @@ +# 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, QIcon, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, QItemSelectionModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, + QVBoxLayout, QToolBar, QAction, QToolButton, +) + +from Modules import Modules + +from View.InitialConditionsAdisTS.UndoCommand import ( + SetCommand, +) + +from View.D90AdisTS.TableDefault import ( + D90TableDefaultModel, +) + +from View.D90AdisTS.Table import ( + D90TableModel, ComboBoxDelegate, +) + +from View.D90AdisTS.translate import D90AdisTSTranslate + +from Solver.Mage import Mage8 + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class D90AdisTSWindow(PamhyrWindow): + _pamhyr_ui = "D90AdisTS" + _pamhyr_name = "D90 AdisTS" + + def __init__(self, data=None, study=None, config=None, parent=None): + self._data = [] + self._data.append(data) + trad = D90AdisTSTranslate() + + name = ( + trad[self._pamhyr_name] + + " - " + study.name + + " - " + self._data[0].name + ) + + super(D90AdisTSWindow, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + self._hash_data.append(data) + + self._d90_adists_lst = study.river.d90_adists + + self.setup_table() + + self.ui.setWindowTitle(self._title) + + def setup_table(self): + + path_icons = os.path.join(self._get_ui_directory(), f"ressources") + + table_default = self.find(QTableView, f"tableView") + + self._table = D90TableDefaultModel( + table_view=table_default, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "d90"], + delegates={}, + data=self._data, + undo=self._undo_stack, + trad=self._trad + ) + + table_default.setModel(self._table) + table_default.setSelectionBehavior(QAbstractItemView.SelectRows) + table_default.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table_default.setAlternatingRowColors(True) + + layout = self.find(QVBoxLayout, f"verticalLayout_1") + toolBar = QToolBar() + layout.addWidget(toolBar) + + action_add = QAction(self) + action_add.setIcon(QIcon(os.path.join(path_icons, f"add.png"))) + action_add.triggered.connect(self.add) + action_delete = QAction(self) + action_delete.setIcon(QIcon(os.path.join(path_icons, f"del.png"))) + action_delete.triggered.connect(self.delete) + + toolBar.addAction(action_add) + toolBar.addAction(action_delete) + + self.table_spec = QTableView() + layout.addWidget(self.table_spec) + + self._delegate_reach = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + ic_spec_lst=self._data[0]._data, + parent=self, + mode="reaches" + ) + self._delegate_kp = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + ic_spec_lst=self._data[0]._data, + parent=self, + mode="kp" + ) + + self._table_spec = D90TableModel( + table_view=self.table_spec, + table_headers=self._trad.get_dict("table_headers_spec"), + editable_headers=["name", "reach", "start_kp", "end_kp", "d90"], + delegates={ + "reach": self._delegate_reach, + "start_kp": self._delegate_kp, + "end_kp": self._delegate_kp + }, + data=self._data[0], + undo=self._undo_stack, + trad=self._trad, + river=self._study.river + ) + + self.table_spec.setModel(self._table_spec) + self.table_spec.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table_spec.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.table_spec.setAlternatingRowColors(True) + + selectionModel = self.table_spec.selectionModel() + index = self.table_spec.model().index(0, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + self.table_spec.scrollTo(index) + + def index_selected_row(self): + #table = self.find(QTableView, f"tableView") + table = self.table_spec + rows = table.selectionModel()\ + .selectedRows() + + if len(rows) == 0: + return 0 + + return rows[0].row() + + def index_selected_rows(self): + #table = self.find(QTableView, f"tableView") + table = self.table_spec + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + 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 add(self): + rows = self.index_selected_rows() + if len(self._data[0]._data) == 0 or len(rows) == 0: + self._table_spec.add(0) + else: + self._table_spec.add(rows[0]) + + def delete(self): + print("del") + rows = self.index_selected_rows() + if len(rows) == 0: + print("len 0") + return + self._table_spec.delete(rows) diff --git a/src/View/DIFAdisTS/translate.py b/src/View/DIFAdisTS/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..5a7bbd755d514a6d628c3828d95883296bf13d95 --- /dev/null +++ b/src/View/DIFAdisTS/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 D90AdisTSTranslate(MainTranslate): + def __init__(self): + super(D90AdisTSTranslate, self).__init__() + + self._dict["D90 AdisTS"] = _translate( + "D90AdisTS", "D90 AdisTS") + + self._dict["kp"] = self._dict["unit_kp"] + + self._sub_dict["table_headers"] = { + "name": self._dict["name"], + "d90": _translate("Unit", "D90"), + } + + self._sub_dict["table_headers_spec"] = { + "name": self._dict["name"], + "reach": self._dict["reach"], + "start_kp": _translate("Unit", "Start_KP (m)"), + "end_kp": _translate("Unit", "End_KP (m)"), + "d90": _translate("Unit", "D90"), + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index c61fae9072100a0ec3c1e952ffa67fe2cb8f2209..ddca58d6bd50fb6c88b20d58dd96d30a14e9447c 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -78,6 +78,7 @@ from View.Debug.Window import ReplWindow from View.OutputKpAdisTS.Window import OutputKpAdisTSWindow from View.Pollutants.Window import PollutantsWindow from View.D90AdisTS.Window import D90AdisTSWindow +from View.DIFAdisTS.Window import D90AdisTSWindow # Optional internal display of documentation for make the application # package lighter... @@ -123,7 +124,7 @@ define_model_action = [ "action_menu_boundary_conditions_sediment", "action_menu_rep_additional_lines", "action_menu_output_kp", "action_menu_run_adists", "action_menu_pollutants", - "action_menu_d90", + "action_menu_d90", "action_menu_dif", ] action = ( @@ -237,6 +238,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): """ actions = { # Menu action + "action_menu_dif": self.open_dif, "action_menu_d90": self.open_d90, "action_menu_pollutants": self.open_pollutants, "action_menu_run_adists":self.run_solver_adists, @@ -888,6 +890,25 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) D90AdisTS.show() + def open_dif(self): + if len(self._study.river.d90_adists.lst) != 0: + d90_default = self._study.river.d90_adists.lst[0] + else: + d90_default = self._study.river.d90_adists.new(0) + + if self.sub_window_exists( + D90AdisTSWindow, + data=[self._study, None, d90_default] + ): + return + + D90AdisTS = D90AdisTSWindow( + study=self._study, + parent=self, + data=d90_default + ) + D90AdisTS.show() + def open_pollutants(self): if self.sub_window_exists( PollutantsWindow, diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 842e220a6ea27679719f9ddcbe2890e64f8db161..c2bda3ebb0d78d3f0a76d722413cfe8c88a49a7b 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -220,6 +220,7 @@ <addaction name="action_menu_output_kp"/> <addaction name="action_menu_pollutants"/> <addaction name="action_menu_d90"/> + <addaction name="action_menu_dif"/> </widget> <addaction name="menu_File"/> <addaction name="menu_network"/> @@ -767,6 +768,11 @@ <string>D90</string> </property> </action> + <action name="action_menu_dif"> + <property name="text"> + <string>DIF</string> + </property> + </action> </widget> <resources/> <connections>