diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py new file mode 100644 index 0000000000000000000000000000000000000000..3b89089f68c45e5de460bb7deb5a1617b269b748 --- /dev/null +++ b/src/Model/InitialConditions/InitialConditions.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +from copy import copy +from tools import trace, timer + +class Data(object): + def __init__(self, status = None): + super(Data, self).__init() + + self._status = status + + self._name = "" + self._comment = "" + + self._kp = 0.0 + self._flow = 0.0 + self._cote = 0.0 + self._tiran = 0.0 + + def __getitems__(self, key): + val = None + + if key == "name": + val = self._name + elif key == "comment": + val = self._comment + elif key == "kp": + val = self._kp + elif key == "flow": + val = self._flow + elif key == "cote": + val = self._cote + elif key == "tiran": + val = self._tiran + + return val + + def __setitems__(self, key, value): + if key == "name": + self._name = str(value) + elif key == "comment": + self._comment = str(value) + elif key == "kp": + self._kp = float(value) + elif key == "flow": + self._flow = float(value) + elif key == "cote": + self._cote = float(value) + elif key == "tiran": + self._tiran = float(value) + + self._status.modified() + +class InitialConditions(object): + def __init__(self, reach = None, status = None): + super(InitialConditions, self).__init__() + + self._status = status + + self._reach = reach + self._data = [] + + def __len__(self): + return len(self._data) + + @property + def reach(self): + return self._reach + + @reach.setter + def reach(self, new): + self._reach = reach + self._status.modified() + + @property + def data(self): + return self._data.copy() + + def get(self, index): + return self._data[index] + + def set(self, index, data): + self._data.insert(index, data) + self._status.modified() + + def new(self, index): + n = Data(self._status) + self._data.insert(index, n) + self._status.modified() + + def insert(self, index, data): + self._data.insert(index, data) + self._status.modified() + + def delete(self, data): + self._data = list( + filter( + lambda x: x in data, + self._data + ) + ) + self._status.modified() + + def delete_i(self, indexes): + data = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] in indexes, + enumerate(self._data) + ) + ) + ) + self.delete(data) + + def sort(self, reverse=False, key=None): + self._data.sort(reverse=reverse, key=key) + self._status.modified() diff --git a/src/Model/InitialConditions/InitialConditionsDict.py b/src/Model/InitialConditions/InitialConditionsDict.py new file mode 100644 index 0000000000000000000000000000000000000000..dedabac2287662c01394b7a64f442f461e854b64 --- /dev/null +++ b/src/Model/InitialConditions/InitialConditionsDict.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from copy import copy +from tools import trace, timer + +from Model.InitialConditions.InitialConditions import InitialConditions + +class InitialConditionsDict(object): + def __init__(self, status = None): + super(InitialConditionsDict, self).__init__() + + self._status = status + + self._reach = {} + + def __len__(self): + return len(self._reach) + + def is_defined(self, reach): + return reach in self._reach + + def get(self, reach): + if reach in self._reach: + return self._reach[reach] + + new = self.new(reach) + self.set(reach, new) + return new + + def set(self, reach, new): + self._reach[reach] = new + self._status.modified() + + def new(self, reach): + new = InitialConditions(reach = reach, status = self._status) + self.set(reach, new) diff --git a/src/Model/River.py b/src/Model/River.py index 7011d73e3ba71eb55fa073000a156401a04a0b74..cc87c75f2f71fe1e3058905e835d785b9aab3b92 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -9,6 +9,7 @@ from Model.Geometry.Reach import Reach from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList from Model.LateralContribution.LateralContributionList import LateralContributionList +from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict from Model.Stricklers.StricklersList import StricklersList from Model.Section.SectionList import SectionList @@ -62,6 +63,7 @@ class River(Graph): self._current_reach = None self._boundary_condition = BoundaryConditionList(status=self._status) self._lateral_contribution = LateralContributionList(status=self._status) + self._initial_conditions = InitialConditionsDict(status=self._status) self._stricklers = StricklersList(status=self._status) self._sections = SectionList(status=self._status) @@ -73,6 +75,10 @@ class River(Graph): def lateral_contribution(self): return self._lateral_contribution + @property + def initial_conditions(self): + return self._initial_conditions + @property def stricklers(self): return self._stricklers diff --git a/src/View/InitialConditions/Table.py b/src/View/InitialConditions/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..525a898b81d453977c4c6862b8123881f941ec9a --- /dev/null +++ b/src/View/InitialConditions/Table.py @@ -0,0 +1,160 @@ +# -*- 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.InitialConditions.UndoCommand import ( + SetCommand, AddCommand, DelCommand, + SortCommand, MoveCommand, PasteCommand, + DuplicateCommand, +) + +from View.InitialConditions.translate import * + +_translate = QCoreApplication.translate + +class TableModel(QAbstractTableModel): + def __init__(self, river=None, reach=None, undo=None): + super(QAbstractTableModel, self).__init__() + self._headers = list(table_headers.keys()) + self._river = river + self._reach = reach + self._undo = undo + self._ics = self._river.initial_conditions.get(reach) + + def flags(self, index): + options = Qt.ItemIsEnabled | Qt.ItemIsSelectable + options |= Qt.ItemIsEditable + + return options + + def rowCount(self, parent): + return len(self._ics) + + 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] is not None: + return self._ics.get(row)[self._headers[column]] + + 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] is not None: + self._undo.push( + SetCommand( + self._ics, row, self._headers[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._ics, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._ics, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def sort(self, _reverse, parent=QModelIndex()): + self.layoutAboutToBeChanged.emit() + + self._undo.push( + SortCommand( + self._ics, 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._ics, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._ics): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._ics, "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/InitialConditions/UndoCommand.py b/src/View/InitialConditions/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..32bff72617fa1a248e0a3130f9ea90f792237d81 --- /dev/null +++ b/src/View/InitialConditions/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.InitialConditions.InitialConditions import InitialConditions +from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict + +class SetCommand(QUndoCommand): + def __init__(self, ics, row, column, new_value): + QUndoCommand.__init__(self) + + self._ics = ics + self._row = row + self._old = self._ics.get(self._row)[column] + self._new = new_value + + def undo(self): + self._ics.get(self._row)[column] = self._old + + def redo(self): + self._ics.get(self._row)[column] = self._new + +class AddCommand(QUndoCommand): + def __init__(self, ics, index): + QUndoCommand.__init__(self) + + self._ics = ics + self._index = index + self._new = None + + def undo(self): + self._ics.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._ics.new(self._index) + else: + self._ics.insert(self._index, self._new) + +class DelCommand(QUndoCommand): + def __init__(self, ics, tab, rows): + QUndoCommand.__init__(self) + + self._ics = ics + self._rows = rows + + self._ic = [] + for row in rows: + self._ic.append((row, self._ics.get(row))) + self._ic.sort() + + def undo(self): + for row, el in self._ic: + self._ics.insert(row, el) + + def redo(self): + self._ics.delete_i(self._rows) + +class SortCommand(QUndoCommand): + def __init__(self, ics, tab, _reverse): + QUndoCommand.__init__(self) + + self._ics = ics + self._reverse = _reverse + + self._old = self._ics.data + self._indexes = None + + def undo(self): + ll = self._ics.data + self._ics.sort( + key=lambda x: self._indexes[ll.index(x)] + ) + + def redo(self): + self._ics.sort( + reverse=self._reverse, + key=lambda x: x.name + ) + if self._indexes is None: + self._indexes = list( + map( + lambda p: self._old.index(p), + self._ics.data + ) + ) + self._old = None + + +class MoveCommand(QUndoCommand): + def __init__(self, ics, tab, up, i): + QUndoCommand.__init__(self) + + self._ics = ics + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._ics.move_up(self._i) + else: + self._ics.move_down(self._i) + + def redo(self): + if self._up: + self._ics.move_up(self._i) + else: + self._ics.move_down(self._i) + + +class PasteCommand(QUndoCommand): + def __init__(self, ics, tab, row, ic): + QUndoCommand.__init__(self) + + self._ics = ics + self._row = row + self._ic = deepcopy(ic) + self._ic.reverse() + + def undo(self): + self._ics.delete(self._ic) + + def redo(self): + for ic in self._ic: + self._ics.insert(self._row, ic) + + +class DuplicateCommand(QUndoCommand): + def __init__(self, ics, tab, rows, ic): + QUndoCommand.__init__(self) + + self._ics = ics + self._rows = rows + self._ic = deepcopy(ic) + self._ic.reverse() + + def undo(self): + self._ics.delete(self._ic) + + def redo(self): + for ic in self._ics: + self._ics.insert(self._rows[0], ic) diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..dbf22ec4c31793face53c6ecc4cce2a629801cb1 --- /dev/null +++ b/src/View/InitialConditions/Window.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from View.ASubWindow import ASubMainWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, +) + +from View.InitialConditions.UndoCommand import ( + SetCommand, AddCommand, DelCommand, + SortCommand, MoveCommand, PasteCommand, + DuplicateCommand, +) + +from View.InitialConditions.Table import TableModel + +from View.Plot.MplCanvas import MplCanvas +from View.InitialConditions.translate import * + +_translate = QCoreApplication.translate + + +class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="Initial condition", + study=None, parent=None): + title = title + " - " + study.name + + super(InitialConditionsWindow, self).__init__( + name=title, ui="InitialConditions", parent=parent + ) + + self._study = study + self._reach = study.river.current_reach() + self._ics = self._study.river.initial_conditions.get(self._reach) + + self.setup_sc() + self.setup_table() + self.setup_graph() + self.setup_connections() + + self.ui.setWindowTitle(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, f"tableView") + self._table = TableModel( + river = self._study.river, + reach = self._reach, + undo = self._undo_stack, + ) + table.setModel(self._table) + + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_graph(self): + print("TODO") + # self.canvas_1 = MplCanvas(width=5, height=4, dpi=100) + # self.canvas_1.setObjectName("canvas_1") + # self.plot_layout_1 = self.find(QVBoxLayout, "verticalLayout_1") + # self.plot_layout_1.addWidget(self.canvas_1) + + # self.plot = PlotXY( + # canvas = self.canvas_1, + # data = None, + # toolbar = None, + # ) + + # self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) + # self.canvas_2.setObjectName("canvas_2") + # self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") + # self.plot_layout_2.addWidget(self.canvas_2) + + # self.plot = PlotXY( + # canvas = self.canvas_2, + # data = None, + # toolbar = None, + # ) + + 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) + + def index_selected_row(self): + table = self.find(QTableView, f"tableView") + return table.selectionModel()\ + .selectedRows()[0]\ + .row() + + def index_selected_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if self._ics.len(tab) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + + def sort(self): + self._table.sort(False) + + def move_up(self): + row = self.index_selected_row() + self._table.move_up(row) + + def move_down(self): + row = self.index_selected_row() + self._table.move_down(row) + + def copy(self): + print("TODO") + + def paste(self): + print("TODO") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() diff --git a/src/View/InitialConditions/translate.py b/src/View/InitialConditions/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..ea88dff3beb3569712b14bc5f48fc3d2834b37a2 --- /dev/null +++ b/src/View/InitialConditions/translate.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "name": _translate("LateralContribution", "Name"), + "comment": _translate("LateralContribution", "Comment"), + "kp": _translate("LateralContribution", "KP (m)"), + "flow": _translate("LateralContribution", "Flow (m³/s)"), + "cote": _translate("LateralContribution", "Cote (m)"), + "tiran": _translate("LateralContribution", "Tiran (m)") +} diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 5c7035303136ec61b3ec63ece7ebcb036a3933cf..bb0f0ce73e5bf6f404acb354936fc1708a3d313b 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -24,6 +24,7 @@ from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow +from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Sections.Window import SectionsWindow @@ -49,6 +50,7 @@ define_model_action = [ "action_toolBar_boundary_cond", "action_toolBar_lateral_contrib", "action_toolBar_spills", "action_toolBar_sections", "action_toolBar_stricklers", "action_toolBar_building", + "action_toolBar_initial_cond", ] action = ( @@ -130,6 +132,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_toolBar_stricklers": self.open_stricklers, "action_toolBar_sections": self.open_sections, "action_toolBar_building": lambda: self.open_dummy("Ouvrages"), + "action_toolBar_initial_cond": self.open_initial_conditions, } for action in actions: @@ -328,28 +331,40 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "into river network window to work on it") def open_boundary_cond(self): - self.bound = BoundaryConditionWindow(study = self.model, parent=self) + self.bound = BoundaryConditionWindow(study = self.model, parent = self) self.bound.show() def open_lateral_contrib(self): - self.lateral = LateralContributionWindow(study = self.model, parent=self) + self.lateral = LateralContributionWindow(study = self.model, parent = self) self.lateral.show() def open_stricklers(self): self.strick = StricklersWindow( study = self.model, config = self.conf, - parent=self + parent = self ) self.strick.show() def open_sections(self): self.sections = SectionsWindow( study = self.model, - parent=self + parent = self ) self.sections.show() + def open_initial_conditions(self): + print("xxx") + if self.model.river.has_current_reach(): + print("yyy") + self.initial = InitialConditionsWindow( + study = self.model, + parent = self + ) + self.initial.show() + print("zzz") + + # TODO: Delete me ! ############### # DUMMY STUFF # diff --git a/src/View/ui/InitialConditions.ui b/src/View/ui/InitialConditions.ui new file mode 100644 index 0000000000000000000000000000000000000000..087dbe020e5d5f11e348872e543549c10b0b772a --- /dev/null +++ b/src/View/ui/InitialConditions.ui @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>889</width> + <height>480</height> + </rect> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QSplitter" name="splitter_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QWidget" name=""> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="pushButton_generate_1"> + <property name="text"> + <string>Generate 1</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pushButton_generate_2"> + <property name="text"> + <string>Generate 2</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTableView" name="tableView"/> + </item> + </layout> + </widget> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="QWidget" name="verticalLayoutWidget"> + <layout class="QVBoxLayout" name="verticalLayout_1"/> + </widget> + <widget class="QWidget" name="verticalLayoutWidget_2"> + <layout class="QVBoxLayout" name="verticalLayout_2"/> + </widget> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>889</width> + <height>22</height> + </rect> + </property> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <widget class="QToolBar" name="toolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="action_add"/> + <addaction name="action_del"/> + <addaction name="action_sort"/> + </widget> + <action name="action_add"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset> + </property> + <property name="text"> + <string>Add</string> + </property> + <property name="toolTip"> + <string>Add new initial condition</string> + </property> + </action> + <action name="action_del"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset> + </property> + <property name="text"> + <string>delete</string> + </property> + <property name="toolTip"> + <string>Delete inital condition</string> + </property> + </action> + <action name="action_sort"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-sort-ascending.png</normaloff>ressources/gtk-sort-ascending.png</iconset> + </property> + <property name="text"> + <string>sort</string> + </property> + <property name="toolTip"> + <string>Sort inital condition</string> + </property> + </action> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 0c77e22fd84be8474fd2636cb745b5e484405a1d..fa4be386d4870e4b18c9937acb38e2971c991ba8 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -302,6 +302,7 @@ <addaction name="separator"/> <addaction name="action_toolBar_building"/> <addaction name="separator"/> + <addaction name="action_toolBar_initial_cond"/> </widget> <action name="action_menu_new"> <property name="checkable"> @@ -861,6 +862,11 @@ <string>French</string> </property> </action> + <action name="action_toolBar_initial_cond"> + <property name="text"> + <string>Initial conditions</string> + </property> + </action> </widget> <resources/> <connections>