diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index cca0ebe9b162229850ef5d7b2edaaecabe83b09a..6101591786b5d7bac23044dfc749b3e75866ece9 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from tools import trace, timer +from tools import trace, timer, old_pamhyr_date_to_timestamp from Model.Except import NotImplementedMethodeError @@ -13,7 +13,21 @@ class BoundaryCondition(object): self._node = None self._data = [] self._header = [] - self._types = [int, float] + self._types = [float, float] + + def __len__(self): + return len(self._data) + + @classmethod + def compatibility(cls): + return ["liquid", "solid", "suspenssion"] + + @classmethod + def time_convert(cls, data): + if type(data) == str and data.count(":") == 3: + return old_pamhyr_date_to_timestamp(data) + + return int(data) @property def name(self): @@ -46,23 +60,49 @@ class BoundaryCondition(object): def data(self): return self._data.copy() + def get_type_column(self, column): + if 0 <= column < 2: + return self._types[column] + return None + + @property def _default_0(self): return self._types[0](0) + @property def _default_1(self): return self._types[1](0.0) def is_define(self): return self._data is not None + def new_from_data(self, header, data): + new_0 = self._default_0 + new_1 = self._default_1 + + if len(header) != 0: + for i in [0,1]: + for j in range(len(header)): + if self._header[i] == header[j]: + if i == 0: + new_0 = self._types[i](data[j].replace(",", ".")) + else: + new_1 = self._types[i](data[j].replace(",", ".")) + else: + new_0 = self._types[0](data[0]) + new_1 = self._types[1](data[1]) + + return (new_0, new_1) + def add(self, index:int): - value = (self.default_0, self_default_1) + value = (self._default_0, self._default_1) self._data.insert(index, value) + return value def insert(self, index:int, value): self._data.insert(index, value) - def delete(self, indexes): + def delete_i(self, indexes): self._data = list( map( lambda e: e[1], @@ -73,12 +113,30 @@ class BoundaryCondition(object): ) ) - def sort(self, _reverse): - self._data.sort(reverse=_reverse) + def delete(self, els): + self._data = list( + filter( + lambda e: e not in els, + self.data + ) + ) + + def sort(self, _reverse=False, key=None): + if key is None: + self._data.sort(reverse=_reverse) + else: + self._data.sort(reverse=_reverse, key=key) def get_i(self, index): return self.data[index] + def get_range(self, _range): + l = [] + for r in _range: + l.append(r) + return l + + def _set_i_c_v(self, index, column, value): v = list(self._data[index]) v[column] = self._types[column](value) @@ -103,3 +161,15 @@ class BoundaryCondition(object): new._set_i_c_v(ind, j, v[i]) return new + + def move_up(self, index): + if index < len(self): + next = index - 1 + d = self._data + d[index], d[next] = d[next], d[index] + + def move_down(self, index): + if index >= 0: + prev = index + 1 + d = self._data + d[index], d[prev] = d[prev], d[index] diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py index a44629d44ff7a18ad686f4441379f77da66aa7e5..4b14d9242e98fb4ba23c951379ef90a6291a04c0 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionList.py +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -11,56 +11,82 @@ from Model.BoundaryCondition.BoundaryConditionTypes import ( TimeOverZ, TimeOverDebit, ZOverDebit ) -class BoundaryConditionList(list): +class BoundaryConditionList(object): def __init__(self): super(BoundaryConditionList, self).__init__() - def new(self, index): + self._tabs = { + "liquid" : [], + "solid" : [], + "suspenssion" : [] + } + + def len(self, lst): + return len(self._tabs[lst]) + + def get_tab(self, lst): + return self._tabs[lst].copy() + + def get(self, lst, row): + return self._tabs[lst][row] + + def set(self, lst, row, new): + self._tabs[lst][row] = new + + def new(self, lst, index): n = NotDefined() - self.insert(index, n) + self._tabs[lst].insert(index, n) return n - def delete(self, bcs): + def insert(self, lst, index, new): + self._tabs[lst].insert(index, new) + + def delete(self, lst, bcs): for bc in bcs: - self.remove(bc) + self._tabs[lst].remove(bc) - def delete_i(self, indexes): + def delete_i(self, lst, indexes): bcs = list( map( lambda x: x[1], filter( lambda x: x[0] in indexes, - enumerate(self) + enumerate(self._tabs[lst]) ) ) ) - self.delete(bcs) + self.delete(lst, bcs) + + def sort(self, lst, reverse=False, key=None): + self._tabs[lst].sort(reverse=reverse, key=key) - def move_up(self, index): - if index < len(self): + def move_up(self, lst, index): + if index < len(self._tabs[lst]): next = index - 1 - self[index], self[next] = self[next], self[index] + l = self._tabs[lst] + l[index], l[next] = l[next], l[index] - def move_down(self, index): + def move_down(self, lst, index): if index >= 0: prev = index + 1 - self[index], self[prev] = self[prev], self[index] + l = self._tabs[lst] + l[index], l[prev] = l[prev], l[index] def __copy__(self): new = BoundaryConditionList() - for bc in self: - new.append(bc) + for l in self._tabs: + new.tabs[l] = self._tabs[l].copy() return new def __deepcopy__(self): new = BoundaryConditionList() - for bc in self: - new.append(deepcopy(bc)) + for l in self._tabs: + new.tabs[l] = self._tabs[l].deepcopy() return new diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py index 209e9ac77bb1dca83576b27901ace0294a2eb9d8..4bc1d53830f920dfe4452bbb99b72d8ff8d385cc 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -10,7 +10,11 @@ class NotDefined(BoundaryCondition): super(NotDefined, self).__init__(name=name) self._type = "ND" - self._header = ["", ""] + self._header = ["x", "y"] + + @property + def _default_0(self): + return 0.0 class PonctualContribution(BoundaryCondition): def __init__(self, name:str = ""): @@ -18,6 +22,11 @@ class PonctualContribution(BoundaryCondition): self._type = "PC" self._header = ["time", "debit"] + self._types = [PonctualContribution.time_convert, float] + + @classmethod + def compatibility(cls): + return ["liquid"] class TimeOverZ(BoundaryCondition): def __init__(self, name:str = ""): @@ -25,6 +34,11 @@ class TimeOverZ(BoundaryCondition): self._type = "TZ" self._header = ["time", "z"] + self._types = [TimeOverZ.time_convert, float] + + @classmethod + def compatibility(cls): + return ["liquid"] class TimeOverDebit(BoundaryCondition): def __init__(self, name:str = ""): @@ -32,6 +46,11 @@ class TimeOverDebit(BoundaryCondition): self._type = "TD" self._header = ["time", "debit"] + self._types = [TimeOverDebit.time_convert, float] + + @classmethod + def compatibility(cls): + return ["liquid"] class ZOverDebit(BoundaryCondition): def __init__(self, name:str = ""): @@ -41,5 +60,10 @@ class ZOverDebit(BoundaryCondition): self._header = ["z", "debit"] self._types = [float, float] + @classmethod + def compatibility(cls): + return ["liquid"] + + @property def _default_0(self): return 0.0 diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 0fff437ce1cb305e7cddefb828bfcc8bb3a6cf73..16ee09f893ff81e818fabaea0e9f7df4570fbb9c 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -16,7 +16,7 @@ from Model.Geometry.ProfileXYZ import ProfileXYZ from Model.Except import FileFormatError, exception_message_box class Reach: - def __init__(self, parent): + def __init__(self, parent=None): self._parent = parent self._profiles: List[Profile] = [] @@ -39,6 +39,9 @@ class Reach: @property def name(self): + if self._parent == None: + return "" + return self._parent.name @property diff --git a/src/Model/Study.py b/src/Model/Study.py index 59a2ec9c7deff56f9bfabcff844271c8552da2e6..f1a501310c19e08164d6172ce381aaf9f750e67d 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -13,6 +13,9 @@ class Study(Serializable): # Study general information self.name = "" self.description = "" + # Time system + self._time_system = "time" + self._date = datetime.fromtimestamp(0) self.creation_date = datetime.now() self.last_modification_date = datetime.now() @@ -21,13 +24,35 @@ class Study(Serializable): # Study data self.river = None + @property + def time_system(self): + return self._time_system + + def use_time(self): + self._time_system = "time" + + def use_date(self, date:datetime): + self._time_system = "date" + self._date = date + + @property + def date(self): + return self._date + + @date.setter + def date(self, timestamp): + self._date = timestamp + @classmethod def new(cls): return cls() @classmethod - def new(cls, name, description): + def new(cls, name, description, date = None): me = cls() me.name = name me.description = description + if date is not None: + me.use_date() + me.date = date return me diff --git a/src/View/ASubWindow.py b/src/View/ASubWindow.py index 3e1a138f4ff633124331c45e949815c728a77932..61a77987ef0ff4a0419e21831daf79549afbda0c 100644 --- a/src/View/ASubWindow.py +++ b/src/View/ASubWindow.py @@ -4,6 +4,7 @@ import os import csv from io import StringIO +from datetime import datetime from tools import trace @@ -16,9 +17,10 @@ from PyQt5.QtWidgets import ( QTimeEdit, QSpinBox, QTextEdit, QRadioButton, QComboBox, QFileDialog, QMessageBox, QTableView, QAction, + QDateTimeEdit, QWidget, ) from PyQt5.QtCore import ( - QTime, + QTime, QDateTime, ) from PyQt5.uic import loadUi @@ -350,6 +352,31 @@ class ASubWindowFeatures(object): """ return self.find(QComboBox, name).currentText() + def get_datetime_edit(self, name:str): + """Get datetime of datetime edit + + Args: + name: The datetime edit component name + + Returns: + The datetime + """ + return self.find(QDateTimeEdit, name).dateTime().toPyDateTime() + + def set_datetime_edit(self, name:str, date:datetime): + """Set datetime of a datetime edit + + Args: + name: The datetime edit component name + date: The new datetime + + Returns: + Nothing + """ + qdate = QDateTime.fromString(date.isoformat(), "yyyy-MM-ddThh:mm:ss") + self.find(QDateTimeEdit, name).setDateTime(qdate) + + # Top level interface class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): @@ -382,7 +409,6 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): return self.ui.findChild(qtype, name) - class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): def __init__(self, name="", ui="dummy", parent=None): super(ASubWindow, self).__init__(parent=parent) @@ -412,3 +438,27 @@ class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): qtype = self._qtype_from_component_name(name) return self.ui.findChild(qtype, name) + +class AWidget(QWidget, ASubWindowFeatures): + def __init__(self, ui="", parent=None): + super(AWidget, self).__init__(parent=parent) + self.ui = loadUi( + os.path.join(os.path.dirname(__file__), "ui", "Widgets", f"{ui}.ui"), + self + ) + self.parent = parent + + def find(self, qtype, name): + """Find an ui component + + Args: + qtype: Type of QT component + name: Name for component + + Returns: + return the component + """ + if qtype is None: + qtype = self._qtype_from_component_name(name) + + return self.ui.findChild(qtype, name) diff --git a/src/View/BoundaryCondition/BCUndoCommand.py b/src/View/BoundaryCondition/BCUndoCommand.py index dfddf82cbd315c01875b9b41b3dea9efba8516c1..7b1c7f5e995c97dc91997b6c50f0723c0698084e 100644 --- a/src/View/BoundaryCondition/BCUndoCommand.py +++ b/src/View/BoundaryCondition/BCUndoCommand.py @@ -11,105 +11,114 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList class SetNameCommand(QUndoCommand): - def __init__(self, lst, index, new_value): + def __init__(self, bcs, tab, index, new_value): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._index = index - self._old = self._lst[self._index].name + self._old = self._bcs.get(self._tab, self._index).name self._new = new_value def undo(self): - self._lst[self._index].name = self._old + self._bcs.get(self._tab, self._index).name = self._old def redo(self): - self._lst[self._index].name = self._new + self._bcs.get(self._tab, self._index).name = self._new class SetNodeCommand(QUndoCommand): - def __init__(self, lst, index, node): + def __init__(self, bcs, tab, index, node): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._index = index - self._old = self._lst[index].node + self._old = self._bcs.get(self._tab, self._index).node self._new = node def undo(self): - self._lst[self._index].node = self._old + self._bcs.get(self._tab, self._index).node = self._old def redo(self): - self._lst[self._index].node = self._new + self._bcs.get(self._tab, self._index).node = self._new class SetTypeCommand(QUndoCommand): - def __init__(self, lst, index, _type): + def __init__(self, bcs, tab, index, _type): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._index = index self._type = _type - self._old = self._lst[index] - self._new = self._lst[index].convert(self._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._lst[self._index] = self._old + self._bcs.set(self._tab, self._index, self._old) def redo(self): - self._lst[self._index] = self._new + self._bcs.set(self._tab, self._index, self._new) class AddCommand(QUndoCommand): - def __init__(self, lst, index): + def __init__(self, bcs, tab, index): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._index = index self._new = None def undo(self): - self._lst.delete_i([self._index]) + self._bcs.delete_i(self._tab, [self._index]) def redo(self): if self._new is None: - self._new = self._lst.new(self._index) + self._new = self._bcs.new(self._tab, self._index) else: - self._lst.insert(self._index, self._new) + self._bcs.insert(self._tab, self._index, self._new) class DelCommand(QUndoCommand): - def __init__(self, lst, rows): + def __init__(self, bcs, tab, rows): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._rows = rows self._bc = [] for row in rows: - self._bc.append((row, self._lst[row])) + self._bc.append((row, self._bcs.get(self._tab, row))) self._bc.sort() def undo(self): for row, el in self._bc: - self._lst.insert(row, el) + self._bcs.insert(self._tab, row, el) def redo(self): - self._lst.delete_i(self._rows) + self._bcs.delete_i(self._tab, self._rows) class SortCommand(QUndoCommand): - def __init__(self, lst, _reverse): + def __init__(self, bcs, tab, _reverse): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._reverse = _reverse - self._old = self._lst.copy() + self._old = self._bcs.get_tab(self._tab) self._indexes = None def undo(self): - ll = self._lst.copy() - self._lst.sort( + ll = self._bcs.get_tab(self._tab) + self._bcs.sort( + self._tab, key=lambda x: self._indexes[ll.index(x)] ) def redo(self): - self._lst.sort( + self._bcs.sort( + self._tab, reverse=self._reverse, key=lambda x: x.name ) @@ -117,62 +126,65 @@ class SortCommand(QUndoCommand): self._indexes = list( map( lambda p: self._old.index(p), - self._lst + self._bcs.get_tab(self._tab) ) ) self._old = None class MoveCommand(QUndoCommand): - def __init__(self, lst, up, i): + def __init__(self, bcs, tab, up, i): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._up = up == "up" self._i = i def undo(self): if self._up: - self._lst.move_up(self._i) + self._bcs.move_up(self._tab, self._i) else: - self._lst.move_down(self._i) + self._bcs.move_down(self._tab, self._i) def redo(self): if self._up: - self._lst.move_up(self._i) + self._bcs.move_up(self._tab, self._i) else: - self._lst.move_down(self._i) + self._bcs.move_down(self._tab, self._i) class PasteCommand(QUndoCommand): - def __init__(self, lst, row, bc): + def __init__(self, bcs, tab, row, bc): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._row = row self._bc = deepcopy(bc) self._bc.reverse() def undo(self): - self._lst.delete(self._bc) + self._bcs.delete(self._tab, self._bc) def redo(self): for bc in self._bc: - self._lst.insert(self._row, bc) + self._bcs.insert(self._tab, self._row, bc) class DuplicateCommand(QUndoCommand): - def __init__(self, lst, rows, bc): + def __init__(self, bcs, tab, rows, bc): QUndoCommand.__init__(self) - self._lst = lst + self._bcs = bcs + self._tab = tab self._rows = rows self._bc = deepcopy(bc) self._bc.reverse() def undo(self): - self._lst.delete(self._bc) + self._bcs.delete(self._tab, self._bc) def redo(self): - for profile in self._profiles: - self._lst.insert(self._rows[0], profile) + for bc in self._bcs: + self._bcs.insert(self._tab, self._rows[0], bc) diff --git a/src/View/BoundaryCondition/BoundaryConditionWindow.py b/src/View/BoundaryCondition/BoundaryConditionWindow.py index de56722de107521295e2079646edaa885638ce18..9a22cf812de9c352a18ccc38d00b52ffc0df2ca6 100644 --- a/src/View/BoundaryCondition/BoundaryConditionWindow.py +++ b/src/View/BoundaryCondition/BoundaryConditionWindow.py @@ -19,7 +19,7 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QPushButton, QLineEdit, QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, - QComboBox, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, ) from View.BoundaryCondition.BCUndoCommand import ( @@ -27,231 +27,21 @@ from View.BoundaryCondition.BCUndoCommand import ( AddCommand, DelCommand, SortCommand, MoveCommand, PasteCommand, DuplicateCommand, ) + from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, TimeOverZ, TimeOverDebit, ZOverDebit ) -from View.BoundaryCondition.EditBoundaryConditionWindow import EditBoundaryConditionWindow - -_translate = QCoreApplication.translate - -long_types = { - "ND": _translate("BoundaryCondition", "Not defined"), - "PC": _translate("BoundaryCondition", "Ponctual contribution"), - "TZ": _translate("BoundaryCondition", "Time over Z"), - "TD": _translate("BoundaryCondition", "Time over Debit"), - "ZD": _translate("BoundaryCondition", "Z over Debit"), -} - -table_headers = { - "name": _translate("BoundaryCondition", "Name"), - "type": _translate("BoundaryCondition", "Type"), - "node": _translate("BoundaryCondition", "Node") -} - -BC_types = { - "ND": NotDefined, - "PC": PonctualContribution, - "TZ": TimeOverZ, - "TD": TimeOverDebit, - "ZD": ZOverDebit -} - - -class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, mode="type", parent=None): - super(ComboBoxDelegate, self).__init__(parent) - - self._data = data - self._mode = mode - - def createEditor(self, parent, option, index): - self.editor = QComboBox(parent) - - if self._mode == "type": - self.editor.addItems([long_types[k] for k in BC_types.keys()]) - else: - self.editor.addItems( - [_translate("BoundaryCondition", "Not associate")] + - 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 and 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(QAbstractTableModel): - def __init__(self, data=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._data = data - self._undo = undo - self._lst = self._data.boundary_condition - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._lst) - - def columnCount(self, parent): - return len(self._headers) - - 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[row].name - elif self._headers[column] == "type": - t = self._lst[row].bctype - return long_types[t] - elif self._headers[column] == "node": - n = self._lst[row].node - if n is None: - return _translate("BoundaryCondition", "Not associate") - return n.name - - return QVariant() - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[section]] - - 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() - - if self._headers[column] == "name": - self._undo.push( - SetNameCommand( - self._lst, row, value - ) - ) - elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) - self._undo.push( - SetTypeCommand( - self._lst, row, BC_types[key] - ) - ) - elif self._headers[column] == "node": - self._undo.push( - SetNodeCommand( - self._lst, row, self._data.node(value) - ) - ) - - 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() - - 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_stack.push( - MoveCommand( - self._lst, "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, "down", row - ) - ) +from View.BoundaryCondition.Table import ( + TableModel, ComboBoxDelegate +) - self.endMoveRows() - self.layoutChanged.emit() +from View.Network.GraphWidget import GraphWidget +from View.BoundaryCondition.translate import * +from View.BoundaryCondition.Edit.Window import EditBoundaryConditionWindow - def undo(self): - self._undo.undo() - self.layoutChanged.emit() - - def redo(self): - self._undo.redo() - self.layoutChanged.emit() +_translate = QCoreApplication.translate class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): @@ -261,10 +51,11 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): ) self._study = study - self._lst = self._study.river.boundary_condition + self._bcs = self._study.river.boundary_condition self.setup_sc() self.setup_table() + self.setup_graph() self.setup_connections() self.ui.setWindowTitle(title) @@ -278,32 +69,50 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.paste_sc = QShortcut(QKeySequence.Paste, self) def setup_table(self): - table = self.find(QTableView, "tableView") - self._table = TableModel( - data = self._study.river, - undo = self._undo_stack - ) - table.setModel(self._table) + self._table = {} + + for t in ["liquid", "solid", "suspenssion"]: + table = self.find(QTableView, f"tableView_{t}") + self._table[t] = TableModel( + data = self._study.river, + undo = self._undo_stack, + tab = t, + ) + table.setModel(self._table[t]) - self._delegate_type = ComboBoxDelegate( - data = self._study.river, - mode = "type" - ) - self._delegate_node = ComboBoxDelegate( - data = self._study.river, - mode = "node" - ) + self._delegate_type = ComboBoxDelegate( + data = self._study.river, + mode = "type", + tab = t, + parent=self + ) + self._delegate_node = ComboBoxDelegate( + data = self._study.river, + mode = "node", + tab = t, + parent=self + ) - table.setItemDelegateForColumn( - 1, self._delegate_type - ) - table.setItemDelegateForColumn( - 2, self._delegate_node - ) + table.setItemDelegateForColumn( + 1, self._delegate_type + ) + table.setItemDelegateForColumn( + 2, self._delegate_node + ) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.setAlternatingRowColors(True) + 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) @@ -316,16 +125,24 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.copy_sc.activated.connect(self.copy) self.paste_sc.activated.connect(self.paste) + def current_tab(self): + return self.find(QTabWidget, "tabWidget")\ + .currentWidget()\ + .objectName()\ + .replace("tab_", "") def index_selected_row(self): - table = self.find(QTableView, "tableView") + tab = self.current_tab() + table = self.find(QTableView, f"tableView_{tab}") return table.selectionModel()\ .selectedRows()[0]\ .row() def index_selected_rows(self): - table = self.find(QTableView, "tableView") + tab = self.current_tab() + table = self.find(QTableView, f"tableView_{tab}") return list( + # Delete duplicate set( map( lambda i: i.row(), @@ -335,30 +152,34 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): ) def add(self): + tab = self.current_tab() rows = self.index_selected_rows() - if len(self._lst) == 0 or len(rows) == 0: - self._table.add(0) + if self._bcs.len(tab) == 0 or len(rows) == 0: + self._table[tab].add(0) else: - self._table.add(rows[0]) + self._table[tab].add(rows[0]) def delete(self): + tab = self.current_tab() rows = self.index_selected_rows() if len(rows) == 0: return - self._table.delete(rows) + self._table[tab].delete(rows) def sort(self): - self._table.sort(False) + tab = self.current_tab() + self._table[tab].sort(False) def move_up(self): + tab = self.current_tab() row = self.index_selected_row() - self._table.move_up(row) + self._table[tab].move_up(row) def move_down(self): + tab = self.current_tab() row = self.index_selected_row() - self._table.move_down(row) - + self._table[tab].move_down(row) def copy(self): print("TODO") @@ -367,16 +188,20 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): print("TODO") def undo(self): - self._table.undo() + tab = self.current_tab() + self._table[tab].undo() def redo(self): - self._table.redo() + tab = self.current_tab() + self._table[tab].redo() def edit(self): + tab = self.current_tab() rows = self.index_selected_rows() for row in rows: win = EditBoundaryConditionWindow( - data=self._lst[row], + data=self._bcs.get(tab, row), + study=self._study, parent=self ) win.show() diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py new file mode 100644 index 0000000000000000000000000000000000000000..66878b7c8840c48fb782fda34a2b9f47ff2d5df3 --- /dev/null +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime + +from tools import timer, trace +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +from View.BoundaryCondition.Edit.translate import * + +_translate = QCoreApplication.translate + +class Plot(APlot): + def __init__(self, canvas=None, data=None, + mode = "time", toolbar=None): + super(Plot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + self._mode = mode + + def custom_ticks(self): + t0 = datetime.fromtimestamp(0) + nb = len(self.data.data) + mod = int(nb / 5) + + fx = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] % mod == 0, + enumerate(self.data.data) + ) + ) + ) + xx = list(map(lambda v: v[0], fx)) + if self._mode == "time": + xt = list( + map( + lambda v: str( + datetime.fromtimestamp(v[0]) - t0 + ).split(",")[0]\ + .replace("days", _translate("BoundaryCondition", "days"))\ + .replace("day", _translate("BoundaryCondition", "day")), + fx + ) + ) + else: + xt = list( + map( + lambda v: str(datetime.fromtimestamp(v[0]).date()), + fx + ) + ) + + self.canvas.axes.set_xticks(ticks=xx, labels=xt, rotation=45) + + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if len(self.data) == 0: + self._init = False + return + + # 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='r', lw=1., + markersize=5, marker='+', + picker=30, + ) + + self.custom_ticks() + + # Plot label + header = self.data.header + self.canvas.axes.set_xlabel( + table_headers[header[0]], color='black', fontsize=10 + ) + self.canvas.axes.set_ylabel( + table_headers[header[1]], color='black', fontsize=10 + ) + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + #self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + 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) + + self.custom_ticks() + + self.canvas.axes.relim() + self.canvas.axes.autoscale() + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..d41a3551b286428d90b59bfaf6315a4b078a8063 --- /dev/null +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- + +from datetime import date, time, datetime, timedelta + +from tools import trace, timer + +from View.ASubWindow import ASubMainWindow, AWidget +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, QTime, +) + +from PyQt5.QtWidgets import ( + QTableView, QAbstractItemView, QSpinBox, + QTimeEdit, QItemDelegate, +) + +from Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, PonctualContribution, + TimeOverZ, TimeOverDebit, ZOverDebit +) + +from View.BoundaryCondition.Edit.UndoCommand import ( + SetDataCommand, AddCommand, DelCommand, + SortCommand, MoveCommand, PasteCommand, + DuplicateCommand, +) +from View.BoundaryCondition.Edit.translate import * + +_translate = QCoreApplication.translate + + +class ExtendedTimeEdit(AWidget): + def __init__(self, parent=None): + super(ExtendedTimeEdit, self).__init__( + ui="extendedTimeEdit", + parent=parent + ) + self.parent = parent + + self.spinBox_days = self.find(QSpinBox, "spinBox_days") + self.timeEdit = self.find(QTimeEdit, "timeEdit") + + def set_time(self, time): + days = 0 + stime = time + + # if ',' in time, time format is 'DD days, HH:MM:SS', + # otherelse is 'HH:MM:SS' + if "," in time: + s = time.strip().split(" ") + days = int(s[0]) + stime = s[-1] + + qtime = QTime.fromString( + stime, + "h:mm:ss" + ) + self.spinBox_days.setValue(days) + self.timeEdit.setTime(qtime) + + def get_time(self): + days = self.spinBox_days.value() + time = self.timeEdit.time().toPyTime() + secs = ( + (time.hour * 3600) + + (time.minute * 60) + + time.second + ) + + return timedelta(days=days, seconds=secs) + + +class ExTimeDelegate(QItemDelegate): + def __init__(self, data=None, mode="time", parent=None): + super(ExTimeDelegate, self).__init__(parent) + + self._data = data + self._mode = mode + + def createEditor(self, parent, option, index): + self.editor = ExtendedTimeEdit(parent=parent) + value = index.data(Qt.DisplayRole) + self.editor.set_time(value) + 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): + time = editor.get_time() + model.setData(index, int(time.total_seconds())) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup and 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(QAbstractTableModel): + def __init__(self, data=None, mode="time", undo=None): + super(QAbstractTableModel, self).__init__() + self._headers = data.header + self._data = data + self._mode = mode + self._undo = undo + + def flags(self, index): + options = Qt.ItemIsEnabled | Qt.ItemIsSelectable + options |= Qt.ItemIsEditable + + return options + + def rowCount(self, parent): + return len(self._data) + + def columnCount(self, parent): + return len(self._headers) + + 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._mode == "time": + t0 = datetime.fromtimestamp(0) + t = datetime.fromtimestamp(v) + value = str(t - t0) + else: + value = str(datetime.fromtimestamp(v)) + else: + value = f"{v}" + + return value + + def headerData(self, section, orientation, role): + if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: + return table_headers[self._headers[section]] + + 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() + + self._undo.push( + SetDataCommand( + self._data, row, column, value + ) + ) + + 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() + + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..b2eb09acfd3648a34e752cc243dc3a5466090820 --- /dev/null +++ b/src/View/BoundaryCondition/Edit/UndoCommand.py @@ -0,0 +1,147 @@ +# -*- 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 + +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] + self._new = 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 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(self._bcs) + + def redo(self): + for bc in self._bcs: + self._data.insert(self._row, bc) + + +class DuplicateCommand(QUndoCommand): + def __init__(self, data, rows, bc): + QUndoCommand.__init__(self) + + self._data = data + self._rows = rows + self._bc = deepcopy(bc) + self._bc.reverse() + + def undo(self): + self._data.delete(self._bc) + + def redo(self): + for bc in self._bcs: + self._data.insert(self._rows[0], bc) diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..8cd8fbad90e08d07013c05b2a12861ed3d40e4d4 --- /dev/null +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +from View.ASubWindow import ASubMainWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, +) + +from View.Plot.MplCanvas import MplCanvas + +from View.BoundaryCondition.translate import long_types +from View.BoundaryCondition.Edit.Table import TableModel, ExTimeDelegate +from View.BoundaryCondition.Edit.Plot import Plot + +_translate = QCoreApplication.translate + +class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="Edit BoundaryConditions", + data=None, study=None, parent=None): + super(EditBoundaryConditionWindow, self).__init__( + name=title, ui="EditBoundaryConditions", parent=parent + ) + + self._data = data + self._study = study + self._title = title + + self.setup_window() + self.setup_sc() + self.setup_table() + self.setup_plot() + self.setup_connections() + + def setup_window(self): + if self._data is not None: + node_name = (self._data.node.name if self._data.node is not None + else _translate("BoundaryCondition", "Not associate")) + title = ( + _translate("BoundaryCondition", self._title) + + f" - {self._data.name} " + + f"({long_types[self._data.bctype]} - {node_name})" + ) + self.ui.setWindowTitle(title) + else: + self.ui.setWindowTitle(_translate("BoundaryCondition", self._title)) + + def setup_sc(self): + self._undo_stack = QUndoStack() + + self.undo_sc = QShortcut(QKeySequence.Undo, self) + self.redo_sc = QShortcut(QKeySequence.Redo, self) + self.copy_sc = QShortcut(QKeySequence.Copy, self) + self.paste_sc = QShortcut(QKeySequence.Paste, self) + + def setup_table(self): + table = self.find(QTableView, "tableView") + self._table = TableModel( + data = self._data, + undo = self._undo_stack, + mode = self._study.time_system + ) + + if self._data.header[0] == "time": + self._delegate_time = ExTimeDelegate( + data = self._data, + mode = self._study.time_system, + parent = self + ) + + table.setItemDelegateForColumn( + 0, self._delegate_time + ) + + 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.verticalLayout.addWidget(self.canvas) + + self.plot = Plot( + canvas = self.canvas, + data = self._data, + mode = self._study.time_system, + ) + 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.undo_sc.activated.connect(self.undo) + self.redo_sc.activated.connect(self.redo) + self.copy_sc.activated.connect(self.copy) + self.paste_sc.activated.connect(self.paste) + + self._table.dataChanged.connect(self.update) + + 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() + + 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() + + def redo(self): + self._table.redo() + self.plot.update() diff --git a/src/View/BoundaryCondition/Edit/translate.py b/src/View/BoundaryCondition/Edit/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..9ab28f9c5f68c323b6ebb9c4405cdef40adc8911 --- /dev/null +++ b/src/View/BoundaryCondition/Edit/translate.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "x": _translate("BoundaryCondition", "X"), + "y": _translate("BoundaryCondition", "Y"), + "time": _translate("BoundaryCondition", "Time"), + "debit": _translate("BoundaryCondition", "Debit"), + "z": _translate("BoundaryCondition", "Z (m)") +} diff --git a/src/View/BoundaryCondition/EditBoundaryConditionWindow.py b/src/View/BoundaryCondition/EditBoundaryConditionWindow.py deleted file mode 100644 index 3695d260b90fffbf5939734f7c25815ef5e97fd9..0000000000000000000000000000000000000000 --- a/src/View/BoundaryCondition/EditBoundaryConditionWindow.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- - -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow - -from PyQt5.QtCore import ( - Qt, QVariant, QAbstractTableModel, QCoreApplication, -) - -from PyQt5.QtWidgets import ( - QDialogButtonBox, QPushButton, QLineEdit, - QFileDialog, QTableView, QAbstractItemView, -) - -_translate = QCoreApplication.translate - -class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit BoundaryConditions", data=None, parent=None): - super(EditBoundaryConditionWindow, self).__init__( - name=title, ui="EditBoundaryConditions", parent=parent - ) - - self._data = data - self._title = title - - self.setup_window() - - def setup_window(self): - if self._data is not None: - node_name = (self._data.node.name if self._data.node is not None - else _translate("BoundaryCondition", "Not associate")) - title = ( - _translate("BoundaryCondition", self._title) + - f"{self._data.name} " + - f"({self._data.name} - {node_name})" - ) - self.ui.setWindowTitle(title) - else: - self.ui.setWindowTitle(_translate("BoundaryCondition", self._title)) diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..ead3ed7a76c43702af16cb48650a89dc10f30fe8 --- /dev/null +++ b/src/View/BoundaryCondition/Table.py @@ -0,0 +1,239 @@ +# -*- coding: utf-8 -*- + +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.BoundaryCondition.BCUndoCommand import ( + SetNameCommand, SetNodeCommand, SetTypeCommand, + AddCommand, DelCommand, SortCommand, + MoveCommand, PasteCommand, DuplicateCommand, +) + +from Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, PonctualContribution, + TimeOverZ, TimeOverDebit, ZOverDebit +) +from View.BoundaryCondition.translate import * + +_translate = QCoreApplication.translate + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, mode="type", tab="", parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._mode = mode + self._tab = tab + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + if self._mode == "type": + lst = list( + map( + lambda k: long_types[k], + filter( + lambda k: self._tab in BC_types[k].compatibility(), + BC_types.keys() + ) + ) + ) + self.editor.addItems( + lst + ) + else: + self.editor.addItems( + [_translate("BoundaryCondition", "Not associate")] + + 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 and 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(QAbstractTableModel): + def __init__(self, data=None, undo=None, tab=""): + super(QAbstractTableModel, self).__init__() + self._headers = list(table_headers.keys()) + self._data = data + self._undo = undo + self._tab = tab + self._bcs = self._data.boundary_condition + + def flags(self, index): + options = Qt.ItemIsEnabled | Qt.ItemIsSelectable + options |= Qt.ItemIsEditable + + return options + + def rowCount(self, parent): + return self._bcs.len(self._tab) + + def columnCount(self, parent): + return len(self._headers) + + 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._bcs.get(self._tab, row).name + elif self._headers[column] == "type": + t = self._bcs.get(self._tab, row).bctype + return long_types[t] + elif self._headers[column] == "node": + n = self._bcs.get(self._tab, row).node + if n is None: + return _translate("BoundaryCondition", "Not associate") + return n.name + + return QVariant() + + def headerData(self, section, orientation, role): + if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: + return table_headers[self._headers[section]] + + 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() + + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._bcs, self._tab,row, value + ) + ) + elif self._headers[column] == "type": + key = next(k for k, v in long_types.items() if v == value) + self._undo.push( + SetTypeCommand( + self._bcs, self._tab,row, BC_types[key] + ) + ) + elif self._headers[column] == "node": + self._undo.push( + SetNodeCommand( + self._bcs, self._tab,row, self._data.node(value) + ) + ) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._bcs, 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._bcs, self._tab,rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def sort(self, _reverse, parent=QModelIndex()): + self.layoutAboutToBeChanged.emit() + + self._undo.push( + SortCommand( + self._bcs, 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._bcs, self._tab,"up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._bcs): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._bcs, 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/BoundaryCondition/translate.py b/src/View/BoundaryCondition/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..762ca867bde99eaa0b53d2d16fb244366842758e --- /dev/null +++ b/src/View/BoundaryCondition/translate.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, PonctualContribution, + TimeOverZ, TimeOverDebit, ZOverDebit +) + +_translate = QCoreApplication.translate + +long_types = { + "ND": _translate("BoundaryCondition", "Not defined"), + "PC": _translate("BoundaryCondition", "Ponctual contribution"), + "TZ": _translate("BoundaryCondition", "Time over Z"), + "TD": _translate("BoundaryCondition", "Time over Debit"), + "ZD": _translate("BoundaryCondition", "Z over Debit"), +} + +table_headers = { + "name": _translate("BoundaryCondition", "Name"), + "type": _translate("BoundaryCondition", "Type"), + "node": _translate("BoundaryCondition", "Node") +} + +BC_types = { + "ND": NotDefined, + "PC": PonctualContribution, + "TZ": TimeOverZ, + "TD": TimeOverDebit, + "ZD": ZOverDebit +} diff --git a/src/View/Main/NewStudyWindow.py b/src/View/Main/NewStudyWindow.py index 905815ea78555f794d86d2404d80d2bf9e33eacf..6d313199e55034d67ef9cb46362649d401eaf443 100644 --- a/src/View/Main/NewStudyWindow.py +++ b/src/View/Main/NewStudyWindow.py @@ -3,6 +3,11 @@ from Model.Study import Study from View.ASubWindow import ASubWindow +from PyQt5.QtWidgets import ( + QRadioButton, QLabel, QDateTimeEdit, +) + + class NewStudyWindow(ASubWindow): def __init__(self, study=None, title="New Study", parent=None): super(NewStudyWindow, self).__init__(name=title, ui="NewStudy", parent=parent) @@ -13,16 +18,50 @@ class NewStudyWindow(ASubWindow): self.study = study if not self.study is None: - self.set_line_edit_text("lineEdit_name", study.name) - self.set_text_edit_text("textEdit_description", study.description) + self.set_line_edit_text("lineEdit_name", self.study.name) + self.set_text_edit_text("textEdit_description", self.study.description) + self.set_datetime_edit("dateTimeEdit_date", self.study.date) + if self.study.time_system == "date": + self.set_radio_button("radioButton_date", True) + self.find(QLabel, "label_date").setEnabled(True) + self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True) + + self.connection() + + def connection(self): + time = self.find(QRadioButton, "radioButton_time") + date = self.find(QRadioButton, "radioButton_date") + + time.toggled.connect(self.set_time) + date.toggled.connect(self.set_date) + + def set_time(self): + if self.get_radio_button("radioButton_time"): + self.find(QLabel, "label_date").setEnabled(False) + self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(False) + + def set_date(self): + if self.get_radio_button("radioButton_date"): + self.find(QLabel, "label_date").setEnabled(True) + self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True) + def accept(self): name = self.get_line_edit_text("lineEdit_name") description = self.get_text_edit_text("textEdit_description") if self.study is None: - self.parent.set_model(Study.new(name, description)) + study = Study.new(name, description) + if self.get_radio_button("radioButton_date"): + date = self.get_datetime_edit("dateTimeEdit_date") + study.use_date(date) + self.parent.set_model(study) else: self.study.name = name self.study.description = description + if self.get_radio_button("radioButton_date"): + date = self.get_datetime_edit("dateTimeEdit_date") + self.study.use_date(date) + else: + self.study.use_time() self.done(True) diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index e6281c0f06ca566633fe581efc80bd4a7ef81151..522f3210648f44e0ee384f1e1e62ff99a4bd9419 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -79,12 +79,18 @@ class NodeItem(QGraphicsItem): def mousePressEvent(self, event): self.update() - super(NodeItem, self).mousePressEvent(event) + if not self.graph._only_display: + super(NodeItem, self).mousePressEvent(event) def mouseReleaseEvent(self, event): self.update() super(NodeItem, self).mouseReleaseEvent(event) + def mouseMoveEvent(self, event): + self.update() + if not self.graph._only_display: + super(NodeItem, self).mouseMoveEvent(event) + class EdgeItem(QGraphicsItem): Type = QGraphicsItem.UserType + 2 @@ -252,12 +258,15 @@ class GraphWidget(QGraphicsView): changeEdge = pyqtSignal(object) changeNode = pyqtSignal(object) - def __init__(self, graph, parent=None): + def __init__(self, graph, parent=None, + min_size=(400, 400), max_size=None, + size=None, only_display=False): super(GraphWidget, self).__init__(parent=parent) self.timerId = 0 self.parent = parent self._state = "move" + self._only_display = only_display self.graph = graph @@ -286,7 +295,13 @@ class GraphWidget(QGraphicsView): self.scale(1, 1) self.previousScale = 1 - self.setMinimumSize(400, 400) + + if min_size: + self.setMinimumSize(*min_size) + if max_size: + self.setMaximumSize(*max_size) + if size: + self.resize(*size) self.create_items() diff --git a/src/View/ui/BoundaryConditions.ui b/src/View/ui/BoundaryConditions.ui index 8a0f8f962687b9a6df0057fbc166206932967c99..3097fd6363951c9822296f9a089f2e188e04d762 100644 --- a/src/View/ui/BoundaryConditions.ui +++ b/src/View/ui/BoundaryConditions.ui @@ -26,16 +26,44 @@ <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0"> <widget class="QSplitter" name="splitter"> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> - <widget class="QTableView" name="tableView"/> + <widget class="QTabWidget" name="tabWidget"> + <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 class="QWidget" name="tab_solid"> + <attribute name="title"> + <string>Solid</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QTableView" name="tableView_solid"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_suspenssion"> + <attribute name="title"> + <string>Suspenssion</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="0"> + <widget class="QTableView" name="tableView_suspenssion"/> + </item> + </layout> + </widget> + </widget> <widget class="QWidget" name="verticalLayoutWidget"> <layout class="QVBoxLayout" name="verticalLayout"/> </widget> diff --git a/src/View/ui/NewStudy.ui b/src/View/ui/NewStudy.ui index 1805c10eda84fab293b4e6245ac96cc6ff8c6540..95100b5c6594521cba6e300ffc64775cefd5bfb7 100644 --- a/src/View/ui/NewStudy.ui +++ b/src/View/ui/NewStudy.ui @@ -13,60 +13,107 @@ <property name="windowTitle"> <string>Dialog</string> </property> - <layout class="QGridLayout" name="gridLayout_2"> + <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0"> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Name</string> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Description</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="lineEdit_name"> - <property name="text"> - <string>MyNewStudy</string> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QTextEdit" name="textEdit_description"> - <property name="autoFillBackground"> - <bool>false</bool> - </property> - <property name="tabChangesFocus"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Name</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QLineEdit" name="lineEdit_name"> + <property name="text"> + <string>MyNewStudy</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Time system</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <widget class="QRadioButton" name="radioButton_time"> + <property name="text"> + <string>Time</string> </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + <property name="checked"> + <bool>true</bool> </property> </widget> </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QRadioButton" name="radioButton_date"> + <property name="text"> + <string>Date</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_date"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Staring date</string> + </property> + </widget> + </item> + <item> + <widget class="QDateTimeEdit" name="dateTimeEdit_date"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="displayFormat"> + <string>dd/MM/yyyy HH:mm:ss</string> + </property> + </widget> + </item> + </layout> + </item> </layout> </item> + <item row="2" column="0" colspan="2"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Description</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QTextEdit" name="textEdit_description"> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="tabChangesFocus"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="3" column="0" colspan="3"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> </layout> </widget> <resources/> diff --git a/src/View/ui/Widgets/extendedTimeEdit.ui b/src/View/ui/Widgets/extendedTimeEdit.ui new file mode 100644 index 0000000000000000000000000000000000000000..493a96a11eece3431108500c8eff0555f019ea81 --- /dev/null +++ b/src/View/ui/Widgets/extendedTimeEdit.ui @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Form</class> + <widget class="QWidget" name="Form"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>178</width> + <height>44</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QSpinBox" name="spinBox_days"> + <property name="frame"> + <bool>false</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>days</string> + </property> + </widget> + </item> + <item> + <widget class="QTimeEdit" name="timeEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frame"> + <bool>false</bool> + </property> + <property name="displayFormat"> + <string>HH:mm:ss</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/tools.py b/src/tools.py index d2f119174dbb528d7056e30e47b90478927481bf..be41a5b35c5b4d2db1b3b0ccb17882555cb0a8b2 100644 --- a/src/tools.py +++ b/src/tools.py @@ -131,3 +131,25 @@ def flatten(lst): return [] return reduce(list.__add__, lst) + +def old_pamhyr_date_to_timestamp(date:str): + v = date.split(":") + if len(v) != 4: + return 0 + + m = [ + (24 * 60 * 60), # Day to sec + (60 * 60), # Hour to sec + 60, # Minute to sec + 1 # Sec + ] + + ts = reduce( + lambda acc, x: acc + x, + map( + lambda v, m: int(v) * int(m), + v, m + ) + ) + + return ts