From 92712dd172f779b8dc25e11dbe82d75c4dea7c97 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 19 Jul 2023 15:11:53 +0200 Subject: [PATCH 01/34] SL: Add sediment layer Model and UI files schemes. --- src/Model/River.py | 7 + src/View/MainWindow.py | 12 +- src/View/SedimentLayers/Edit/Table.py | 0 src/View/SedimentLayers/Edit/UndoCommand.py | 0 src/View/SedimentLayers/Edit/Window.py | 0 src/View/SedimentLayers/Edit/translate.py | 0 .../SedimentLayers/Reach/Profile/Table.py | 0 .../Reach/Profile/UndoCommand.py | 0 .../SedimentLayers/Reach/Profile/Window.py | 0 .../SedimentLayers/Reach/Profile/translate.py | 0 src/View/SedimentLayers/Reach/Table.py | 0 src/View/SedimentLayers/Reach/UndoCommand.py | 0 src/View/SedimentLayers/Reach/Window.py | 0 src/View/SedimentLayers/Reach/translate.py | 0 src/View/SedimentLayers/Table.py | 151 +++++++++++++++++ src/View/SedimentLayers/UndoCommand.py | 97 +++++++++++ src/View/SedimentLayers/Window.py | 156 ++++++++++++++++++ src/View/SedimentLayers/translate.py | 10 ++ src/View/ui/EditSedimentLayers.ui | 114 +++++++++++++ src/View/ui/MainWindow.ui | 18 ++ src/View/ui/ProfileSedimentLayers.ui | 95 +++++++++++ src/View/ui/ReachSedimentLayers.ui | 98 +++++++++++ src/View/ui/SedimentLayersList.ui | 95 +++++++++++ 23 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 src/View/SedimentLayers/Edit/Table.py create mode 100644 src/View/SedimentLayers/Edit/UndoCommand.py create mode 100644 src/View/SedimentLayers/Edit/Window.py create mode 100644 src/View/SedimentLayers/Edit/translate.py create mode 100644 src/View/SedimentLayers/Reach/Profile/Table.py create mode 100644 src/View/SedimentLayers/Reach/Profile/UndoCommand.py create mode 100644 src/View/SedimentLayers/Reach/Profile/Window.py create mode 100644 src/View/SedimentLayers/Reach/Profile/translate.py create mode 100644 src/View/SedimentLayers/Reach/Table.py create mode 100644 src/View/SedimentLayers/Reach/UndoCommand.py create mode 100644 src/View/SedimentLayers/Reach/Window.py create mode 100644 src/View/SedimentLayers/Reach/translate.py create mode 100644 src/View/SedimentLayers/Table.py create mode 100644 src/View/SedimentLayers/UndoCommand.py create mode 100644 src/View/SedimentLayers/Window.py create mode 100644 src/View/SedimentLayers/translate.py create mode 100644 src/View/ui/EditSedimentLayers.ui create mode 100644 src/View/ui/ProfileSedimentLayers.ui create mode 100644 src/View/ui/ReachSedimentLayers.ui create mode 100644 src/View/ui/SedimentLayersList.ui diff --git a/src/Model/River.py b/src/Model/River.py index a19e6cc3..a46661f4 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -15,6 +15,7 @@ from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict from Model.Stricklers.StricklersList import StricklersList from Model.Friction.FrictionList import FrictionList from Model.SolverParameters.SolverParametersList import SolverParametersList +from Model.SedimentLayer.SedimentLayerList import SedimentLayerList from Solver.Solvers import solver_type_list @@ -192,6 +193,7 @@ class River(Graph, SQLSubModel): InitialConditionsDict, StricklersList, SolverParametersList, + SedimentLayerList, ] def __init__(self, status=None): @@ -207,6 +209,7 @@ class River(Graph, SQLSubModel): self._initial_conditions = InitialConditionsDict(status=self._status) self._stricklers = StricklersList(status=self._status) self._parameters = {} + self._sediment_layers = SedimentLayerList() @classmethod def _sql_create(cls, execute): @@ -301,6 +304,10 @@ class River(Graph, SQLSubModel): def initial_conditions(self): return self._initial_conditions + @property + def sediment_layers(self): + return self._sediment_layers + @property def stricklers(self): return self._stricklers diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 10a35a6e..79c7ef67 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -33,6 +33,7 @@ from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow +from View.SedimentLayers.Window import SedimentLayersWindow from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow @@ -70,7 +71,8 @@ define_model_action = [ "action_menu_edit_network", "action_menu_edit_geometry", "action_menu_boundary_conditions", "action_menu_initial_conditions", "action_menu_edit_friction", "action_menu_edit_lateral_contribution", - "action_menu_run_solver", + "action_menu_run_solver", "action_menu_sediment_layers", + "action_menu_edit_reach_sediment_layers" ] action = ( @@ -147,6 +149,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_friction": self.open_frictions, "action_menu_edit_lateral_contribution": self.open_lateral_contrib, "action_menu_run_solver": self.run_solver, + "action_menu_sediment_layers": self.open_sediment_layer, ## Help "action_menu_about": self.open_about, # ToolBar action @@ -490,6 +493,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) params.show() + def open_sediment_layer(self): + sl = SedimentLayersWindow( + study = self.model, + parent = self + ) + sl.show() + def run_solver(self): if self.model is None: return diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Profile/translate.py b/src/View/SedimentLayers/Reach/Profile/translate.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/UndoCommand.py b/src/View/SedimentLayers/Reach/UndoCommand.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Table.py b/src/View/SedimentLayers/Table.py new file mode 100644 index 00000000..7abbd869 --- /dev/null +++ b/src/View/SedimentLayers/Table.py @@ -0,0 +1,151 @@ +# -*- 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.SedimentLayers.UndoCommand import * +from View.SedimentLayers.translate import * + +_translate = QCoreApplication.translate + + +class TableModel(QAbstractTableModel): + def __init__(self, study=None, undo=None): + super(QAbstractTableModel, self).__init__() + self._headers = list(table_headers.keys()) + self._study = study + self._undo = undo + self._sl = self._study.river.sediment_layers + + def flags(self, index): + options = Qt.ItemIsEnabled | Qt.ItemIsSelectable + options |= Qt.ItemIsEditable + + return options + + def rowCount(self, parent): + return len(self._sl) + + 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._sl.get(row).name + elif self._headers[column] == "comment": + return self._sl.get(row).name + + return QVariant() + + def headerData(self, friction, orientation, role): + if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: + return table_headers[self._headers[friction]] + + 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._sl, row, value + ) + ) + if self._headers[column] == "comment": + self._undo.push( + SetCommentCommand( + self._sl, row, 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._sl, row, self._data + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._sl, rows + ) + ) + + self.endRemoveRows() + 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._sl, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._sl): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._sl, "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/SedimentLayers/UndoCommand.py b/src/View/SedimentLayers/UndoCommand.py new file mode 100644 index 00000000..84c4027b --- /dev/null +++ b/src/View/SedimentLayers/UndoCommand.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.SedimentLayer.SedimentLayer import SedimentLayer +from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + +class SetNameCommand(QUndoCommand): + def __init__(self, sediment_layers_list, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers_list = sediment_layers_list + self._index = index + self._old = self._sediment_layers_list.get(self._index).name + self._new = new_value + + def undo(self): + self._sediment_layers_list.get(self._index).name = self._old + + def redo(self): + self._sediment_layers_list.get(self._index).name = self._new + +class SetCommentCommand(QUndoCommand): + def __init__(self, sediment_layers_list, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers_list = sediment_layers_list + self._index = index + self._old = self._sediment_layers_list.get(self._index).comment + self._new = new_value + + def undo(self): + self._sediment_layers_list.get(self._index).comment = self._old + + def redo(self): + self._sediment_layers_list.get(self._index).comment = self._new + +class AddCommand(QUndoCommand): + def __init__(self, sediment_layers_list, index, reach): + QUndoCommand.__init__(self) + + self._sediment_layers_list = sediment_layers_list + self._index = index + self._new = None + + def undo(self): + self._sediment_layers_list.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._sediment_layers_list.new(self._index) + else: + self._sediment_layers_list.insert(self._index, self._new) + +class DelCommand(QUndoCommand): + def __init__(self, sediment_layers_list, rows): + QUndoCommand.__init__(self) + + self._sediment_layers_list = sediment_layers_list + self._rows = rows + + self._sl_pos = [] + for row in rows: + self._sl_pos.append((row, self._sediment_layers_list.get(row))) + self._sl_pos.sort() + + def undo(self): + for row, el in self._sl_pos: + self._sediment_layers_list.insert(row, el) + + def redo(self): + self._sediment_layers_list.delete_i(self._rows) + +class MoveCommand(QUndoCommand): + def __init__(self, sediment_layers_list, up, i): + QUndoCommand.__init__(self) + + self._sediment_layers_list = sediment_layers_list + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._sediment_layers_list.move_up(self._i) + else: + self._sediment_layers_list.move_down(self._i) + + def redo(self): + if self._up: + self._sediment_layers_list.move_up(self._i) + else: + self._sediment_layers_list.move_down(self._i) diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py new file mode 100644 index 00000000..22b61318 --- /dev/null +++ b/src/View/SedimentLayers/Window.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +import logging + +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.SedimentLayers.UndoCommand import * +from View.SedimentLayers.Table import * + +from View.Plot.MplCanvas import MplCanvas +from View.SedimentLayers.translate import * + +# from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="SedimentLayersList", study=None, parent=None): + self._study = study + self._sediment_layers = self._study.river.sediment_layers + + self.setup_title(title) + + super(SedimentLayersWindow, self).__init__( + name=self._title, ui="SedimentLayersList", parent=parent + ) + + self.setup_sc() + self.setup_table() + self.setup_graph() + self.setup_connections() + + self.ui.setWindowTitle(self._title) + + def setup_title(self, title): + self._title = ( + title + " - " + self._study.name + ) + + 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): + self._table = {} + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + study = self._study, + undo = self._undo_stack, + ) + table.setModel(self._table) + + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_graph(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # display_current = False + # ) + # 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_edit").triggered.connect(self.edit_sediment_layers) + + 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_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._sediment_layers) == 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 copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() + + def edit_sediment_layers(self): + rows = self.index_selected_rows() + + for row in rows: + slw = EditSedimentLayersWindow( + study = self._study, + sl = self.__sediment_layers[row], + parent = self + ) + slw.show() diff --git a/src/View/SedimentLayers/translate.py b/src/View/SedimentLayers/translate.py new file mode 100644 index 00000000..6eafe079 --- /dev/null +++ b/src/View/SedimentLayers/translate.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "name": _translate("SedimentLayers", "Name"), + "comment": _translate("SedimentLayers", "Comment"), +} diff --git a/src/View/ui/EditSedimentLayers.ui b/src/View/ui/EditSedimentLayers.ui new file mode 100644 index 00000000..7e236641 --- /dev/null +++ b/src/View/ui/EditSedimentLayers.ui @@ -0,0 +1,114 @@ + + + MainWindow + + + + 0 + 0 + 960 + 480 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 960 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add sediment layer + + + Add a new sediment layer + + + Ctrl+N + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete sediment layer + + + Delete selected sediment layer(s) + + + Ctrl+D + + + + + + ressources/up.pngressources/up.png + + + Move up + + + Move up + + + + + + ressources/down.pngressources/down.png + + + Move down + + + Move down + + + + + + diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index b63f032c..e91bd14d 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -199,10 +199,18 @@ + + + &Sediment + + + + + @@ -909,6 +917,16 @@ Initial conditions + + + Sediment layers + + + + + Edit reach sediment layers + + diff --git a/src/View/ui/ProfileSedimentLayers.ui b/src/View/ui/ProfileSedimentLayers.ui new file mode 100644 index 00000000..b9f5106d --- /dev/null +++ b/src/View/ui/ProfileSedimentLayers.ui @@ -0,0 +1,95 @@ + + + MainWindow + + + + 0 + 0 + 960 + 480 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 960 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add sediment layers + + + Add specific sediment layers on selected point(s) + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete sediment layers + + + Delete specific sediment layers of selected point(s) + + + + + + ressources/edit.pngressources/edit.png + + + Edit sediment layers + + + Edit sediment layers list + + + + + + diff --git a/src/View/ui/ReachSedimentLayers.ui b/src/View/ui/ReachSedimentLayers.ui new file mode 100644 index 00000000..b37db0c4 --- /dev/null +++ b/src/View/ui/ReachSedimentLayers.ui @@ -0,0 +1,98 @@ + + + MainWindow + + + + 0 + 0 + 960 + 480 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + Edit sediment layers list + + + + + + + Apply sediment layers on all reach + + + + + + + + + + + + + + + + 0 + 0 + 960 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + ressources/edit.pngressources/edit.png + + + Edit profile + + + Edit profile sediment layer + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add sediment layer + + + + + + diff --git a/src/View/ui/SedimentLayersList.ui b/src/View/ui/SedimentLayersList.ui new file mode 100644 index 00000000..399b0995 --- /dev/null +++ b/src/View/ui/SedimentLayersList.ui @@ -0,0 +1,95 @@ + + + MainWindow + + + + 0 + 0 + 960 + 480 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 960 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add sediment layer + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete sediment layer + + + Delete sediment layer + + + + + + ressources/edit.pngressources/edit.png + + + Edit sediment layer + + + Edit sediment layer + + + Ctrl+E + + + + + + From 17c0035a0a7fa8b3c19bf99e3465f579e673ab9e Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 21 Jul 2023 11:01:21 +0200 Subject: [PATCH 02/34] SL: Minor change and add missing files... --- src/Model/River.py | 8 +- src/Model/SedimentLayer/SedimentLayer.py | 186 +++++++++++++++++++ src/Model/SedimentLayer/SedimentLayerList.py | 93 ++++++++++ src/View/SedimentLayers/Table.py | 4 +- src/View/SedimentLayers/UndoCommand.py | 2 +- 5 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 src/Model/SedimentLayer/SedimentLayer.py create mode 100644 src/Model/SedimentLayer/SedimentLayerList.py diff --git a/src/Model/River.py b/src/Model/River.py index a46661f4..2ff98bd8 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -209,7 +209,7 @@ class River(Graph, SQLSubModel): self._initial_conditions = InitialConditionsDict(status=self._status) self._stricklers = StricklersList(status=self._status) self._parameters = {} - self._sediment_layers = SedimentLayerList() + self._sediment_layers = SedimentLayerList(status=self._status) @classmethod def _sql_create(cls, execute): @@ -263,6 +263,12 @@ class River(Graph, SQLSubModel): data ) + # Initial conditions + new._sediment_layers = SedimentLayerList._sql_load( + execute, + data + ) + # Parameters new._parameters = SolverParametersList._sql_load( execute, diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py new file mode 100644 index 00000000..47d84df5 --- /dev/null +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from Model.DB import SQLSubModel +from Model.Except import NotImplementedMethodeError + +class Layer(SQLSubModel): + _sub_classes = [] + _id_cnt = 0 + + def __init__(self, + id:int = -1, name:str = "", + type = "", height = 0, + sl = None, status = None): + super(Layer, self).__init__() + + self._status = status + self._sl = sl + + self._name = name + self._type = type + self._height = height + + if id == -1: + self.id = Layer._id_cnt + else: + self.id = id + + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE sedimentary_layer_layer( + id INTEGER NOT NULL PRIMARY KEY, + ind INTEGER NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL, + height INTEGER NOT NULL, + sl INTEGER, + FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + sl = data["sl"] + + table = execute( + "SELECT id, ind, name, type, height " + + "FROM sedimentary_layer_layer " + + f"WHERE sl = {sl.id}" + ) + + for _ in table: + new.append(None) + + for row in table: + ind = row[1] + + layer = cls( + id = row[0], name = row[2], + type = row[3], height = row[4], + sl = sl, status = data['status'] + ) + + new[ind] = layer + + return new + + def _sql_save(self, execute, data = None): + ind = data["ind"] + + sql = ( + "INSERT INTO " + + "sedimentary_layer_layer(id, ind, name, type, height, sl) "+ + "VALUES (" + + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + + f"'{self._sql_format(self._type)}', '{self._height}', {self._sl.id}" + + ")" + ) + execute(sql) + + return True + + +class SedimentLayer(SQLSubModel): + _sub_classes = [Layer] + _id_cnt = 0 + + def __init__(self, id:int = -1, + name:str = "", comment:str = "", + status = None): + super(SedimentLayer, self).__init__() + + self._status = status + self._name = name + self._comment = comment + self._layers = [] + + if id == -1: + self.id = SedimentLayer._id_cnt + else: + self.id = id + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + + @property + def comment(self): + return self._comment + + @comment.setter + def comment(self, comment): + self._comment = comment + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE sedimentary_layer( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + comment TEXT NOT NULL + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + + table = execute( + "SELECT id, name, comment" + + "FROM sedimentary_layer " + ) + + for row in table: + sl = cls( + id = row[0], + name = row[1], + comment = row[2], + status = data['status'] + ) + + data["sl"] = sl.id + sl._layers = Layer._sql_load(execute, data) + + new.append(sl) + + return new + + def _sql_save(self, execute, data = None): + if data is None: + data = {} + + sql = ( + "INSERT INTO sedimentary_layer (id, name) "+ + f"VALUES ({self.id}, '{self._sql_format(self._name)}')" + ) + execute(sql) + + ind = 0 + for l in self._layers: + data["ind"] = ind + l._sql_save(execute, data) + + ind += 1 + + return True diff --git a/src/Model/SedimentLayer/SedimentLayerList.py b/src/Model/SedimentLayer/SedimentLayerList.py new file mode 100644 index 00000000..b360c723 --- /dev/null +++ b/src/Model/SedimentLayer/SedimentLayerList.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from Model.DB import SQLSubModel +from Model.Except import NotImplementedMethodeError +from Model.SedimentLayer.SedimentLayer import SedimentLayer + +class SedimentLayerList(SQLSubModel): + _sub_classes = [SedimentLayer] + + def __init__(self, status = None): + super(SedimentLayerList, self).__init__() + + self._status = status + self._sl = [] + + def __len__(self): + return len(self._sl) + + @classmethod + def _sql_create(cls, execute): + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls(status = data["status"]) + + new._sl = SedimentLayer._sql_load(execute, data) + + return new + + def _sql_save(self, execute, data = None): + ok = True + + for sl in self._sl: + ok &= sl._sql_save(execute, data) + + return ok + + def get(self, index): + return self._sl[index] + + def set(self, index, new): + self._sl[index] = new + self._status.modified() + + def insert(self, index, new): + self._sl.insert(index, new) + self._status.modified() + + def new(self, index): + n = SedimentLayer(status = self._status) + self.insert(index, n) + self._status.modified() + return n + + def delete(self, els): + for el in els: + self._sl.remove(el) + self._status.modified() + + def delete_i(self, indexes): + els = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] in indexes, + enumerate(self._sl) + ) + ) + ) + self.delete(els) + + def move_up(self, index): + if index < len(self._sl): + next = index - 1 + + l = self._sl + l[index], l[next] = l[next], l[index] + self._status.modified() + + def move_down(self, index): + if index >= 0: + prev = index + 1 + + l = self._sl + l[index], l[prev] = l[prev], l[index] + self._status.modified() diff --git a/src/View/SedimentLayers/Table.py b/src/View/SedimentLayers/Table.py index 7abbd869..f375c318 100644 --- a/src/View/SedimentLayers/Table.py +++ b/src/View/SedimentLayers/Table.py @@ -51,7 +51,7 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": return self._sl.get(row).name elif self._headers[column] == "comment": - return self._sl.get(row).name + return self._sl.get(row).comment return QVariant() @@ -89,7 +89,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sl, row, self._data + self._sl, row ) ) diff --git a/src/View/SedimentLayers/UndoCommand.py b/src/View/SedimentLayers/UndoCommand.py index 84c4027b..f785743b 100644 --- a/src/View/SedimentLayers/UndoCommand.py +++ b/src/View/SedimentLayers/UndoCommand.py @@ -41,7 +41,7 @@ class SetCommentCommand(QUndoCommand): self._sediment_layers_list.get(self._index).comment = self._new class AddCommand(QUndoCommand): - def __init__(self, sediment_layers_list, index, reach): + def __init__(self, sediment_layers_list, index): QUndoCommand.__init__(self) self._sediment_layers_list = sediment_layers_list From 7898ab86c633262f6a279affed31bb446b2dfb2d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 21 Jul 2023 14:53:24 +0200 Subject: [PATCH 03/34] SL: Model add basic command for SL and fix DB save artifact. --- src/Model/SedimentLayer/SedimentLayer.py | 51 +++++++++++++++++++- src/Model/SedimentLayer/SedimentLayerList.py | 4 ++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 47d84df5..65684bd8 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -180,7 +180,56 @@ class SedimentLayer(SQLSubModel): for l in self._layers: data["ind"] = ind l._sql_save(execute, data) - ind += 1 return True + + def get(self, index): + return self._layers[index] + + def set(self, index, new): + self._layers[index] = new + self._status.modified() + + def insert(self, index, new): + self._layers.insert(index, new) + self._status.modified() + + def new(self, index): + n = Layer(status = self._status) + self.insert(index, n) + self._status.modified() + return n + + def delete(self, els): + for el in els: + self._layers.remove(el) + self._status.modified() + + def delete_i(self, indexes): + els = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] in indexes, + enumerate(self._layers) + ) + ) + ) + self.delete(els) + + def move_up(self, index): + if index < len(self._layers): + next = index - 1 + + l = self._layers + l[index], l[next] = l[next], l[index] + self._status.modified() + + def move_down(self, index): + if index >= 0: + prev = index + 1 + + l = self._layers + l[index], l[prev] = l[prev], l[index] + self._status.modified() diff --git a/src/Model/SedimentLayer/SedimentLayerList.py b/src/Model/SedimentLayer/SedimentLayerList.py index b360c723..32317c0b 100644 --- a/src/Model/SedimentLayer/SedimentLayerList.py +++ b/src/Model/SedimentLayer/SedimentLayerList.py @@ -37,6 +37,10 @@ class SedimentLayerList(SQLSubModel): def _sql_save(self, execute, data = None): ok = True + # Delete previous data + execute("DELETE FROM sedimentary_layer") + execute("DELETE FROM sedimentary_layer_layer") + for sl in self._sl: ok &= sl._sql_save(execute, data) From e9b00aa148e5caf1dd2b057e8cc9fd9d1fed4dbc Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 21 Jul 2023 16:11:13 +0200 Subject: [PATCH 04/34] SL: Add Edit window and fix DB save and load. --- src/Model/River.py | 1 + src/Model/SedimentLayer/SedimentLayer.py | 45 +++++- src/View/SedimentLayers/Edit/Table.py | 153 ++++++++++++++++++++ src/View/SedimentLayers/Edit/UndoCommand.py | 97 +++++++++++++ src/View/SedimentLayers/Edit/Window.py | 150 +++++++++++++++++++ src/View/SedimentLayers/Edit/translate.py | 11 ++ src/View/SedimentLayers/Window.py | 4 +- src/View/ui/EditSedimentLayers.ui | 8 +- 8 files changed, 456 insertions(+), 13 deletions(-) diff --git a/src/Model/River.py b/src/Model/River.py index 2ff98bd8..59b47957 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -282,6 +282,7 @@ class River(Graph, SQLSubModel): objs.append(self._boundary_condition) objs.append(self._initial_conditions) objs.append(self._lateral_contribution) + objs.append(self._sediment_layers) objs.append(self._stricklers) for solver in self._parameters: diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 65684bd8..7f8f1fa4 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -16,7 +16,6 @@ class Layer(SQLSubModel): super(Layer, self).__init__() self._status = status - self._sl = sl self._name = name self._type = type @@ -27,6 +26,31 @@ class Layer(SQLSubModel): else: self.id = id + Layer._id_cnt = max(id, Layer._id_cnt+1) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + + @property + def type(self): + return self._type + + @type.setter + def type(self, type): + self._type = type + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height @classmethod def _sql_create(cls, execute): @@ -56,7 +80,7 @@ class Layer(SQLSubModel): table = execute( "SELECT id, ind, name, type, height " + "FROM sedimentary_layer_layer " + - f"WHERE sl = {sl.id}" + f"WHERE sl = {sl}" ) for _ in table: @@ -77,13 +101,14 @@ class Layer(SQLSubModel): def _sql_save(self, execute, data = None): ind = data["ind"] + sl = data["sl"] sql = ( "INSERT INTO " + "sedimentary_layer_layer(id, ind, name, type, height, sl) "+ "VALUES (" + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._type)}', '{self._height}', {self._sl.id}" + + f"'{self._sql_format(self._type)}', '{self._height}', {sl}" + ")" ) execute(sql) @@ -110,6 +135,9 @@ class SedimentLayer(SQLSubModel): else: self.id = id + def __len__(self): + return len(self._layers) + @property def name(self): return self._name @@ -147,7 +175,7 @@ class SedimentLayer(SQLSubModel): new = [] table = execute( - "SELECT id, name, comment" + + "SELECT id, name, comment " + "FROM sedimentary_layer " ) @@ -171,11 +199,14 @@ class SedimentLayer(SQLSubModel): data = {} sql = ( - "INSERT INTO sedimentary_layer (id, name) "+ - f"VALUES ({self.id}, '{self._sql_format(self._name)}')" + "INSERT INTO sedimentary_layer (id, name, comment) " + + f"VALUES ({self.id}, '{self._sql_format(self._name)}', " + + f"'{self._sql_format(self._comment)}')" ) execute(sql) + data["sl"] = self + ind = 0 for l in self._layers: data["ind"] = ind @@ -196,7 +227,7 @@ class SedimentLayer(SQLSubModel): self._status.modified() def new(self, index): - n = Layer(status = self._status) + n = Layer(sl=self, status = self._status) self.insert(index, n) self._status.modified() return n diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index e69de29b..6bd59387 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -0,0 +1,153 @@ +# -*- 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.SedimentLayers.Edit.UndoCommand import * +from View.SedimentLayers.Edit.translate import * + +_translate = QCoreApplication.translate + + +class TableModel(QAbstractTableModel): + def __init__(self, study=None, sl=None, undo=None): + super(QAbstractTableModel, self).__init__() + self._headers = list(table_headers.keys()) + self._study = study + self._undo = undo + self._sl = sl + + def flags(self, index): + options = Qt.ItemIsEnabled | Qt.ItemIsSelectable + options |= Qt.ItemIsEditable + + return options + + def rowCount(self, parent): + return len(self._sl) + + 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._sl.get(row).name + elif self._headers[column] == "type": + return self._sl.get(row).type + elif self._headers[column] == "height": + return self._sl.get(row).height + + return QVariant() + + def headerData(self, friction, orientation, role): + if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: + return table_headers[self._headers[friction]] + + 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._sl, row, value + ) + ) + if self._headers[column] == "comment": + self._undo.push( + SetCommentCommand( + self._sl, row, 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._sl, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._sl, rows + ) + ) + + self.endRemoveRows() + 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._sl, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._sl): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._sl, "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/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py index e69de29b..9e630a6e 100644 --- a/src/View/SedimentLayers/Edit/UndoCommand.py +++ b/src/View/SedimentLayers/Edit/UndoCommand.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.SedimentLayer.SedimentLayer import SedimentLayer +from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + +class SetNameCommand(QUndoCommand): + def __init__(self, sediment_layers, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).name + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).name = self._old + + def redo(self): + self._sediment_layers.get(self._index).name = self._new + +class SetHeightCommand(QUndoCommand): + def __init__(self, sediment_layers, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).height + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).height = self._old + + def redo(self): + self._sediment_layers.get(self._index).height = self._new + +class AddCommand(QUndoCommand): + def __init__(self, sediment_layers, index): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._new = None + + def undo(self): + self._sediment_layers.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._sediment_layers.new(self._index) + else: + self._sediment_layers.insert(self._index, self._new) + +class DelCommand(QUndoCommand): + def __init__(self, sediment_layers, rows): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._rows = rows + + self._sl_pos = [] + for row in rows: + self._sl_pos.append((row, self._sediment_layers.get(row))) + self._sl_pos.sort() + + def undo(self): + for row, el in self._sl_pos: + self._sediment_layers.insert(row, el) + + def redo(self): + self._sediment_layers.delete_i(self._rows) + +class MoveCommand(QUndoCommand): + def __init__(self, sediment_layers, up, i): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._sediment_layers.move_up(self._i) + else: + self._sediment_layers.move_down(self._i) + + def redo(self): + if self._up: + self._sediment_layers.move_up(self._i) + else: + self._sediment_layers.move_down(self._i) diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index e69de29b..44100d6f 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import logging + +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.Plot.MplCanvas import MplCanvas + +from View.SedimentLayers.Edit.UndoCommand import * +from View.SedimentLayers.Edit.Table import * +from View.SedimentLayers.Edit.translate import * + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="Edit Sediment Layers", + study=None, sl=None, parent=None): + self._study = study + self._sl = sl + + self.setup_title(title) + + super(EditSedimentLayersWindow, self).__init__( + name=self._title, ui="EditSedimentLayers", parent=parent + ) + + self.setup_sc() + self.setup_table() + self.setup_graph() + self.setup_connections() + + self.ui.setWindowTitle(self._title) + + def setup_title(self, title): + name = self._sl.name + if name == "": + name = _translate("SedimentLayers", "(no name)") + + self._title = ( + title + " - " + self._study.name + " - " + name + ) + + 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): + self._table = {} + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + study = self._study, + sl = self._sl, + undo = self._undo_stack, + ) + table.setModel(self._table) + + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_graph(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # display_current = False + # ) + # 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_move_up").triggered.connect(self.delete) + self.find(QAction, "action_move_down").triggered.connect(self.delete) + + 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_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._sl) == 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 copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py index e69de29b..3b61d18c 100644 --- a/src/View/SedimentLayers/Edit/translate.py +++ b/src/View/SedimentLayers/Edit/translate.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "name": _translate("SedimentLayers", "Name"), + "type": _translate("SedimentLayers", "Type"), + "height": _translate("Sedimentlayers", "Height"), +} diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index 22b61318..b2e8f1fa 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -30,7 +30,7 @@ from View.SedimentLayers.Table import * from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.translate import * -# from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow +from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow _translate = QCoreApplication.translate @@ -150,7 +150,7 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): for row in rows: slw = EditSedimentLayersWindow( study = self._study, - sl = self.__sediment_layers[row], + sl = self._sediment_layers.get(row), parent = self ) slw.show() diff --git a/src/View/ui/EditSedimentLayers.ui b/src/View/ui/EditSedimentLayers.ui index 7e236641..6fe4c11b 100644 --- a/src/View/ui/EditSedimentLayers.ui +++ b/src/View/ui/EditSedimentLayers.ui @@ -49,12 +49,12 @@ false - - + + - + ressources/gtk-add.pngressources/gtk-add.png @@ -69,7 +69,7 @@ Ctrl+N - + ressources/gtk-remove.pngressources/gtk-remove.png From 5a29dca452d7b25e6a4c56ae3473fae9daea7e03 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 11:36:08 +0200 Subject: [PATCH 05/34] SL: Add reach SL edition window. --- src/Model/Geometry/Profile.py | 14 ++ src/Model/SedimentLayer/SedimentLayer.py | 5 +- src/Model/SedimentLayer/SedimentLayerList.py | 4 + src/View/MainWindow.py | 13 +- src/View/SedimentLayers/Reach/Table.py | 151 +++++++++++++++++++ src/View/SedimentLayers/Reach/UndoCommand.py | 32 ++++ src/View/SedimentLayers/Reach/Window.py | 150 ++++++++++++++++++ src/View/SedimentLayers/Reach/translate.py | 11 ++ src/View/SedimentLayers/Window.py | 2 - src/View/ui/ReachSedimentLayers.ui | 13 +- 10 files changed, 379 insertions(+), 16 deletions(-) diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index b0328f80..7836c47f 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -34,6 +34,7 @@ class Profile(object): self._kp = float(kp) self._name = str(name) self._reach = reach + self._sl = None self._points: List[Point] = [] @@ -120,6 +121,19 @@ class Profile(object): self._name = value.strip() self._status.modified() + @property + def sl(self): + """ + Returns: + Profile sediment layers. + """ + return self._sl + + @sl.setter + def sl(self, value: str): + self._sl = value + self._status.modified() + @property def profile_type(self): """ diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 7f8f1fa4..8099aab0 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -108,7 +108,7 @@ class Layer(SQLSubModel): "sedimentary_layer_layer(id, ind, name, type, height, sl) "+ "VALUES (" + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._type)}', '{self._height}', {sl}" + + f"'{self._sql_format(self._type)}', '{self._height}', {sl.id}" + ")" ) execute(sql) @@ -135,6 +135,9 @@ class SedimentLayer(SQLSubModel): else: self.id = id + def __str__(self): + return f"{self.name} ({len(self)}) - {self.comment}" + def __len__(self): return len(self._layers) diff --git a/src/Model/SedimentLayer/SedimentLayerList.py b/src/Model/SedimentLayer/SedimentLayerList.py index 32317c0b..d2b66226 100644 --- a/src/Model/SedimentLayer/SedimentLayerList.py +++ b/src/Model/SedimentLayer/SedimentLayerList.py @@ -46,6 +46,10 @@ class SedimentLayerList(SQLSubModel): return ok + @property + def sediment_layers(self): + return self._sl.copy() + def get(self, index): return self._sl[index] diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 79c7ef67..2c9b1b65 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -34,6 +34,7 @@ from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow +from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow @@ -149,7 +150,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_friction": self.open_frictions, "action_menu_edit_lateral_contribution": self.open_lateral_contrib, "action_menu_run_solver": self.run_solver, - "action_menu_sediment_layers": self.open_sediment_layer, + "action_menu_sediment_layers": self.open_sediment_layers, + "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, ## Help "action_menu_about": self.open_about, # ToolBar action @@ -493,13 +495,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) params.show() - def open_sediment_layer(self): + def open_sediment_layers(self): sl = SedimentLayersWindow( study = self.model, parent = self ) sl.show() + def open_reach_sediment_layers(self): + sl = ReachSedimentLayersWindow( + study = self.model, + parent = self + ) + sl.show() + def run_solver(self): if self.model is None: return diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py index e69de29b..d9cac095 100644 --- a/src/View/SedimentLayers/Reach/Table.py +++ b/src/View/SedimentLayers/Reach/Table.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +import logging + +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.SedimentLayers.Reach.UndoCommand import * +from View.SedimentLayers.Reach.translate import * + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, study=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._study = study + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + self.editor.addItems( + [_translate("SedimentLayers", "Not defined")] + + list( + map( + lambda sl: str(sl), + self._study.river.sediment_layers.sediment_layers + ) + ) + ) + + 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, study=None, reach=None, undo=None): + super(QAbstractTableModel, self).__init__() + self._headers = list(table_headers.keys()) + self._study = study + self._undo = undo + self._reach = reach + + def flags(self, index): + column = index.column() + + options = Qt.ItemIsEnabled | Qt.ItemIsSelectable + if self._headers[column] == "sl": + options |= Qt.ItemIsEditable + + return options + + def rowCount(self, parent): + return self._reach.number_profiles + + 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._reach.profile(row).name + if self._headers[column] == "kp": + return self._reach.profile(row).kp + if self._headers[column] == "sl": + value = self._reach.profile(row).sl + if value == None: + text = _translate("SedimentLayers", "Not defined") + return text + return str(value) + + return QVariant() + + def headerData(self, friction, orientation, role): + if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: + return table_headers[self._headers[friction]] + + 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] == "sl": + new = None + if value != _translate("SedimentLayers", "Not defined"): + new = next( + filter( + lambda sl: str(sl) == value, + self._study.river.sediment_layers.sediment_layers + ) + ) + + self._undo.push( + SetSLCommand( + self._reach, row, new + ) + ) + + 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/SedimentLayers/Reach/UndoCommand.py b/src/View/SedimentLayers/Reach/UndoCommand.py index e69de29b..82ba3764 100644 --- a/src/View/SedimentLayers/Reach/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/UndoCommand.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.Geometry.Reach import Reach +from Model.Geometry.Profile import Profile +from Model.SedimentLayer.SedimentLayer import SedimentLayer +from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + +logger = logging.getLogger() + +class SetSLCommand(QUndoCommand): + def __init__(self, reach, index, new_value): + QUndoCommand.__init__(self) + + self._reach = reach + self._index = index + self._old = self._reach.profile(self._index).sl + self._new = new_value + + def undo(self): + self._reach.profile(self._index).sl = self._old + + def redo(self): + self._reach.profile(self._index).sl = self._new diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index e69de29b..b292db5d 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import logging + +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.SedimentLayers.Reach.UndoCommand import * +from View.SedimentLayers.Reach.Table import * + +from View.Plot.MplCanvas import MplCanvas +from View.SedimentLayers.Reach.translate import * + +from View.SedimentLayers.Window import SedimentLayersWindow + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="Reach sediment layers", study=None, parent=None): + self._study = study + self._sediment_layers = self._study.river.sediment_layers + self._reach = self._study.river.current_reach().reach + + self.setup_title(title) + + super(ReachSedimentLayersWindow, self).__init__( + name=self._title, ui="ReachSedimentLayers", parent=parent + ) + + self.setup_sc() + self.setup_table() + self.setup_graph() + self.setup_connections() + + self.ui.setWindowTitle(self._title) + + def setup_title(self, title): + self._title = ( + title + " - " + self._study.name + " - " + self._reach.name + ) + + 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( + study = self._study, + reach = self._reach, + undo = self._undo_stack, + ) + table.setModel(self._table) + + self._delegate_stricklers = ComboBoxDelegate( + study = self._study, + parent=self + ) + + table.setItemDelegateForColumn( + list(table_headers).index("sl"), + self._delegate_stricklers + ) + + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_graph(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # display_current = False + # ) + # self.plot.draw() + + + def setup_connections(self): + self.find(QAction, "action_edit").triggered.connect(self.edit_profile) + + 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_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() + + def edit_profile(self): + rows = self.index_selected_rows() + + for row in rows: + slw = ProfileSedimentLayersWindow( + study = self._study, + sl = self._sediment_layers.get(row), + parent = self + ) + slw.show() diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py index e69de29b..f6b17cf8 100644 --- a/src/View/SedimentLayers/Reach/translate.py +++ b/src/View/SedimentLayers/Reach/translate.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "name": _translate("SedimentLayers", "Name"), + "kp": _translate("SedimentLayers", "KP (m)"), + "sl": _translate("SedimentLayers", "Sediment layers"), +} diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index b2e8f1fa..b04c073d 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -68,8 +68,6 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.paste_sc = QShortcut(QKeySequence.Paste, self) def setup_table(self): - self._table = {} - table = self.find(QTableView, f"tableView") self._table = TableModel( study = self._study, diff --git a/src/View/ui/ReachSedimentLayers.ui b/src/View/ui/ReachSedimentLayers.ui index b37db0c4..a1beb8ca 100644 --- a/src/View/ui/ReachSedimentLayers.ui +++ b/src/View/ui/ReachSedimentLayers.ui @@ -69,9 +69,9 @@ false - + - + ressources/edit.pngressources/edit.png @@ -83,15 +83,6 @@ Edit profile sediment layer - - - - ressources/gtk-add.pngressources/gtk-add.png - - - Add sediment layer - - From 1b1bbb6d02ad2fd370d5711166ad82bb1b33cdf3 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 15:38:05 +0200 Subject: [PATCH 06/34] SL: Add profile edition. --- src/Model/Geometry/Point.py | 10 ++ src/Model/Geometry/Profile.py | 3 + .../SedimentLayers/Reach/Profile/Table.py | 155 ++++++++++++++++++ .../Reach/Profile/UndoCommand.py | 32 ++++ .../SedimentLayers/Reach/Profile/Window.py | 148 +++++++++++++++++ .../SedimentLayers/Reach/Profile/translate.py | 13 ++ src/View/SedimentLayers/Reach/Window.py | 3 +- 7 files changed, 363 insertions(+), 1 deletion(-) diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index 2108d962..041603df 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -9,6 +9,16 @@ class Point(object): self._status = status self._name = name + self._sl = None + + + @property + def sl(self): + return self._sl + + @sl.setter + def sl(self, sl): + self._sl = sl @property def name(self): diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 7836c47f..483082b0 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -48,6 +48,9 @@ class Profile(object): def points(self): return self._points.copy() + def point(self, index): + return self._points[index] + @property def reach(self): return self._reach diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py index e69de29b..2675e8ee 100644 --- a/src/View/SedimentLayers/Reach/Profile/Table.py +++ b/src/View/SedimentLayers/Reach/Profile/Table.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +import logging + +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.SedimentLayers.Reach.Profile.UndoCommand import * +from View.SedimentLayers.Reach.Profile.translate import * + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, study=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._study = study + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + self.editor.addItems( + [_translate("SedimentLayers", "Not defined")] + + list( + map( + lambda sl: str(sl), + self._study.river.sediment_layers.sediment_layers + ) + ) + ) + + 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, study=None, profile=None, undo=None): + super(QAbstractTableModel, self).__init__() + self._headers = list(table_headers.keys()) + self._study = study + self._undo = undo + self._profile = profile + + def flags(self, index): + column = index.column() + + options = Qt.ItemIsEnabled | Qt.ItemIsSelectable + if self._headers[column] == "sl": + options |= Qt.ItemIsEditable + + return options + + def rowCount(self, parent): + return self._profile.number_points + + 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._profile.point(row).name + elif self._headers[column] == "sl": + value = self._profile.point(row).sl + if value == None: + text = _translate("SedimentLayers", "Not defined") + return text + return str(value) + elif self._headers[column] == "x": + return self._profile.point(row).x + elif self._headers[column] == "y": + return self._profile.point(row).y + elif self._headers[column] == "z": + return self._profile.point(row).z + + return QVariant() + + def headerData(self, friction, orientation, role): + if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: + return table_headers[self._headers[friction]] + + 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] == "sl": + new = None + if value != _translate("SedimentLayers", "Not defined"): + new = next( + filter( + lambda sl: str(sl) == value, + self._study.river.sediment_layers.sediment_layers + ) + ) + + self._undo.push( + SetSLCommand( + self._profile, row, new + ) + ) + + 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/SedimentLayers/Reach/Profile/UndoCommand.py b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py index e69de29b..fe3497ae 100644 --- a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.Geometry.Reach import Reach +from Model.Geometry.Profile import Profile +from Model.SedimentLayer.SedimentLayer import SedimentLayer +from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + +logger = logging.getLogger() + +class SetSLCommand(QUndoCommand): + def __init__(self, profile, index, new_value): + QUndoCommand.__init__(self) + + self._profile = profile + self._index = index + self._old = self._profile.point(self._index).sl + self._new = new_value + + def undo(self): + self._profile.point(self._index).sl = self._old + + def redo(self): + self._profile.point(self._index).sl = self._new diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index e69de29b..8f15a27e 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +import logging + +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.SedimentLayers.Reach.Profile.UndoCommand import * +from View.SedimentLayers.Reach.Profile.Table import * + +from View.Plot.MplCanvas import MplCanvas +from View.SedimentLayers.Reach.Profile.translate import * + +from View.SedimentLayers.Window import SedimentLayersWindow + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="Profile sediment layers", study=None, profile=None, parent=None): + self._study = study + self._sediment_layers = self._study.river.sediment_layers + self._profile = profile + self._reach = self._study.river.current_reach().reach + + self.setup_title(title) + + super(ProfileSedimentLayersWindow, self).__init__( + name=self._title, ui="ProfileSedimentLayers", parent=parent + ) + + self.setup_sc() + self.setup_table() + self.setup_graph() + self.setup_connections() + + self.ui.setWindowTitle(self._title) + + def setup_title(self, title): + rname = self._reach.name + if rname == "": + rname = _translate("SedimentLayers", "(no name)") + + pname = self._profile.name + if pname == "": + pname = _translate("SedimentLayers", "(no name)") + + self._title = ( + title + " - " + + self._study.name + " - " + + rname + " - " + pname + ) + + 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( + study = self._study, + profile = self._profile, + undo = self._undo_stack, + ) + table.setModel(self._table) + + self._delegate_stricklers = ComboBoxDelegate( + study = self._study, + parent=self + ) + + table.setItemDelegateForColumn( + list(table_headers).index("sl"), + self._delegate_stricklers + ) + + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_graph(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # display_current = False + # ) + # self.plot.draw() + + + def setup_connections(self): + 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_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() diff --git a/src/View/SedimentLayers/Reach/Profile/translate.py b/src/View/SedimentLayers/Reach/Profile/translate.py index e69de29b..33e204b3 100644 --- a/src/View/SedimentLayers/Reach/Profile/translate.py +++ b/src/View/SedimentLayers/Reach/Profile/translate.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "x": _translate("SedimentLayers", "X (m)"), + "y": _translate("SedimentLayers", "Y (m)"), + "z": _translate("SedimentLazers", "Z (m)"), + "name": _translate("SedimentLayers", "Name"), + "sl": _translate("SedimentLayers", "Sediment layers"), +} diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index b292db5d..711a04fc 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -31,6 +31,7 @@ from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.Reach.translate import * from View.SedimentLayers.Window import SedimentLayersWindow +from View.SedimentLayers.Reach.Profile.Window import ProfileSedimentLayersWindow _translate = QCoreApplication.translate @@ -144,7 +145,7 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): for row in rows: slw = ProfileSedimentLayersWindow( study = self._study, - sl = self._sediment_layers.get(row), + profile = self._reach.profile(row), parent = self ) slw.show() From 3397754abc4aaf2c0423ad8379ec2238730393dd Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 16:01:53 +0200 Subject: [PATCH 07/34] SL: Fix SL edition command. --- src/View/SedimentLayers/Edit/Table.py | 10 ++++++++-- src/View/SedimentLayers/Edit/UndoCommand.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index 6bd59387..32790f1b 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -76,9 +76,15 @@ class TableModel(QAbstractTableModel): self._sl, row, value ) ) - if self._headers[column] == "comment": + if self._headers[column] == "type": self._undo.push( - SetCommentCommand( + SetTypeCommand( + self._sl, row, value + ) + ) + if self._headers[column] == "height": + self._undo.push( + SetHeightCommand( self._sl, row, value ) ) diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py index 9e630a6e..e6964709 100644 --- a/src/View/SedimentLayers/Edit/UndoCommand.py +++ b/src/View/SedimentLayers/Edit/UndoCommand.py @@ -25,6 +25,21 @@ class SetNameCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).name = self._new +class SetTypeCommand(QUndoCommand): + def __init__(self, sediment_layers, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).type + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).type = self._old + + def redo(self): + self._sediment_layers.get(self._index).type = self._new + class SetHeightCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) From d3edf9e5c314d9d7ab7ab08bdff0924417d7c86c Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 17:16:37 +0200 Subject: [PATCH 08/34] Geometry: Propagate profile information into point. --- src/Model/Geometry/Point.py | 3 ++- src/Model/Geometry/PointXYZ.py | 11 ++++++----- src/Model/Geometry/ProfileXYZ.py | 14 +++++++------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index 041603df..cdebd17e 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -3,12 +3,13 @@ from Model.Except import NotImplementedMethodeError class Point(object): - def __init__(self, name:str = "", status = None): + def __init__(self, name:str = "", profile=None, status = None): super(Point, self).__init__() self._status = status self._name = name + self._profile = profile self._sl = None diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 0fecd1ea..4e55c7b8 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -10,8 +10,8 @@ class PointXYZ(Point, SQLSubModel): _sub_classes = [] def __init__(self, x:float = 0.0, y:float = 0.0, z:float = 0.0, - name:str = "", status = None): - super(PointXYZ, self).__init__(name=name, status=status) + name:str = "", profile = None, status = None): + super(PointXYZ, self).__init__(name=name, profile=profile, status=status) self._x = float(x) self._y = float(y) @@ -49,7 +49,7 @@ class PointXYZ(Point, SQLSubModel): table = execute( "SELECT ind, name, x, y, z " + "FROM geometry_pointXYZ " + - f"WHERE profile = {profile}" + f"WHERE profile = {profile.id}" ) # Create points list @@ -67,6 +67,7 @@ class PointXYZ(Point, SQLSubModel): new = cls( name = name, x = x, y = y, z = z, + profile = profile, status = status ) @@ -84,7 +85,7 @@ class PointXYZ(Point, SQLSubModel): "VALUES (" + f"{ind}, '{self._sql_format(self._name)}', " + f"{self.x}, {self.y}, {self.z}, " + - f"{profile}" + + f"{profile.id}" + ")" ) execute(sql) @@ -101,7 +102,7 @@ class PointXYZ(Point, SQLSubModel): *data ) else: - valid_header = {'name', 'x', 'y', 'z'} + valid_header = {'name', 'x', 'y', 'z', 'profile'} d = {} for i, v in enumerate(data): h = header[i].strip().lower().split(' ')[0] diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index d368b981..af310769 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -102,7 +102,7 @@ class ProfileXYZ(Profile, SQLSubModel): status = status ) - data["profile"] = id + data["profile"] = new new._points = PointXYZ._sql_load(execute, data) profiles[ind] = new @@ -124,7 +124,7 @@ class ProfileXYZ(Profile, SQLSubModel): ) execute(sql) - data["profile"] = self.id + data["profile"] = self execute(f"DELETE FROM geometry_pointXYZ WHERE profile = {self.id}") ind = 0 @@ -162,11 +162,11 @@ class ProfileXYZ(Profile, SQLSubModel): try: if len(header) == 0: point = PointXYZ( - *data, status=self._status + *data, profile=self, status=self._status ) else: valid_header = {'name', 'x', 'y', 'z'} - d = {"status": self._status} + d = {"status": self._status, "profile": self} for i, v in enumerate(data): h = header[i].strip().lower().split(' ')[0] if h in valid_header: @@ -219,7 +219,7 @@ class ProfileXYZ(Profile, SQLSubModel): Nothing. """ for point in list_points: - pt = PointXYZ(*point, status=self._status) + pt = PointXYZ(*point, profile=self, status=self._status) self._points.append(pt) self._status.modified() @@ -243,7 +243,7 @@ class ProfileXYZ(Profile, SQLSubModel): Returns: Nothing. """ - point_xyz = PointXYZ(0., 0., 0., status=self._status) + point_xyz = PointXYZ(0., 0., 0., profile=self, status=self._status) self._points.append(point_xyz) self._status.modified() @@ -256,7 +256,7 @@ class ProfileXYZ(Profile, SQLSubModel): Returns: The new point. """ - point = PointXYZ(0., 0., 0., status=self._status) + point = PointXYZ(0., 0., 0., profile=self, status=self._status) self._points.insert(index, point) self._status.modified() return point From 550d86683da78e7abc64b6d82f1758e550afcc40 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 17:55:51 +0200 Subject: [PATCH 09/34] SL: Propagate profile SL to point and fix SL id cnt. --- src/Model/Geometry/Point.py | 6 ++++++ src/Model/SedimentLayer/SedimentLayer.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index cdebd17e..98d1e348 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -15,10 +15,16 @@ class Point(object): @property def sl(self): + if self._sl is None: + return self._profile.sl + return self._sl @sl.setter def sl(self, sl): + if sl == self._profile.sl: + self._sl = None + self._sl = sl @property diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 8099aab0..c0c34abc 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -135,6 +135,8 @@ class SedimentLayer(SQLSubModel): else: self.id = id + SedimentLayer._id_cnt = max(id, SedimentLayer._id_cnt+1) + def __str__(self): return f"{self.name} ({len(self)}) - {self.comment}" From 5fab694c2705adf7e50dfe549b48421fa9ddd617 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 09:41:05 +0200 Subject: [PATCH 10/34] Geometry: Plot: Rename KPC to KPZ. --- src/View/Frictions/Window.py | 6 +++--- src/View/Geometry/{PlotKPC.py => PlotKPZ.py} | 8 ++++---- src/View/Geometry/Window.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/View/Geometry/{PlotKPC.py => PlotKPZ.py} (97%) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 71a3e8e1..18b39e3c 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -33,7 +33,7 @@ from View.Frictions.Table import ( ) from View.Plot.MplCanvas import MplCanvas -from View.Geometry.PlotKPC import PlotKPC +from View.Geometry.PlotKPZ import PlotKPZ from View.Frictions.PlotStricklers import PlotStricklers from View.Frictions.translate import * @@ -112,7 +112,7 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - self.plot = PlotKPC( + self.plot = PlotKPZ( canvas = self.canvas, data = self._reach.reach, toolbar = None, @@ -177,7 +177,7 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): sec = self._frictions.get(rows[0]) highlight = (sec.begin_kp, sec.end_kp) - self.plot = PlotKPC( + self.plot = PlotKPZ( canvas = self.canvas, data = reach, toolbar = None, diff --git a/src/View/Geometry/PlotKPC.py b/src/View/Geometry/PlotKPZ.py similarity index 97% rename from src/View/Geometry/PlotKPC.py rename to src/View/Geometry/PlotKPZ.py index f3e883f3..b37f988f 100644 --- a/src/View/Geometry/PlotKPC.py +++ b/src/View/Geometry/PlotKPZ.py @@ -13,10 +13,10 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotKPC(APlot): +class PlotKPZ(APlot): def __init__(self, canvas=None, data=None, toolbar=None, display_current=True): - super(PlotKPC, self).__init__( + super(PlotKPZ, self).__init__( canvas=canvas, data=data, toolbar=toolbar @@ -48,7 +48,7 @@ class PlotKPC(APlot): color='green', fontsize=12 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Cote (m)"), + _translate("MainWindow_reach", "Height (m)"), color='green', fontsize=12 ) @@ -218,7 +218,7 @@ class PlotKPC(APlot): kp, z_complete[i] ) except Exception as e: - logger.warning(f"Failed to update graphic KPC: {e}") + logger.warning(f"Failed to update graphic KPZ: {e}") self.canvas.axes.autoscale_view(True, True, True) self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 8efea1c3..835db523 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -22,7 +22,7 @@ from PyQt5.QtWidgets import ( ) from View.Geometry.PlotXY import PlotXY -from View.Geometry.PlotKPC import PlotKPC +from View.Geometry.PlotKPZ import PlotKPZ from View.Geometry.PlotAC import PlotAC from View.ASubWindow import WindowToolKit @@ -201,7 +201,7 @@ class GeometryWindow(QMainWindow, WindowToolKit): def plot_kpc(self): self.tableView.model().blockSignals(True) - self._plot_kpc = PlotKPC( + self._plot_kpc = PlotKPZ( canvas = self.ui.canvas_2, data = self._reach, toolbar = self.ui.toolbar_2 From ebd05768a40047de3af70f0674a686156fdc8659 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 11:35:32 +0200 Subject: [PATCH 11/34] SL: Add reach sediment layers plot. --- src/Model/Geometry/Reach.py | 33 ++++++++ src/Model/SedimentLayer/SedimentLayer.py | 4 +- src/View/SedimentLayers/Reach/Plot.py | 101 +++++++++++++++++++++++ src/View/SedimentLayers/Reach/Window.py | 15 ++-- 4 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 src/View/SedimentLayers/Reach/Plot.py diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index b18f2ade..728cf2db 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -260,6 +260,39 @@ class Reach(SQLSubModel): return max(self.get_kp()) + # Sediment Layers + + def get_sl(self): + """Get sediment layer height of profile + + Get sediment layer of profile (without spesific point sl) + + Returns: + List of sediment layers height + """ + res = [] + psl = [profile.sl for profile in self.profiles] + + # Compute max number of layers + sl_max = 0 + for sl in psl: + n = 0 if sl is None else len(sl) + sl_max = max(n, sl_max) + + # Create list of height for each sl and each layer + for i in range(0, sl_max): + cur = [] + # Compute new layer line for each sl + for sl in psl: + if sl is not None and i < len(sl): + cur.append(sl.get(i).height) + else: + cur.append(0) + # Add layer line to result + res.append(cur) + + return res + # Guidelines @timer diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index c0c34abc..f1b8a88a 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -50,7 +50,7 @@ class Layer(SQLSubModel): @height.setter def height(self, height): - self._height = height + self._height = float(height) @classmethod def _sql_create(cls, execute): @@ -60,7 +60,7 @@ class Layer(SQLSubModel): ind INTEGER NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL, - height INTEGER NOT NULL, + height REAL NOT NULL, sl INTEGER, FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) ) diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py new file mode 100644 index 00000000..af7a54ed --- /dev/null +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class Plot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None, + display_current=True): + super(Plot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + self._display_current = display_current + + self.line_kp_zmin = None + self.line_kp_sl = [] + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + if self.data.number_profiles == 0: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "Kp (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + kp = self.data.get_kp() + sl = self.data.get_sl() + z_min = self.data.get_z_min() + z_max = self.data.get_z_max() + + self.canvas.axes.set_xlim( + left = min(kp), right = max(kp) + ) + + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl, + [z_min] + ) + + for i, z in enumerate(z_sl): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + kp, z, + linestyle="solid" if i == 0 else "--", + lw=1.8, + color='grey' if i == 0 else None + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + if ind is None: + kp = self.data.get_kp() + z_min = self.data.get_z_min() + z_max = self.data.get_z_max() + + self.line_kp_zmin.set_data(kp, z_min) + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 711a04fc..4d155cec 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.Reach.UndoCommand import * from View.SedimentLayers.Reach.Table import * +from View.SedimentLayers.Reach.Plot import Plot from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.Reach.translate import * @@ -98,13 +99,13 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout_2") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # display_current = False - # ) - # self.plot.draw() + self.plot = Plot( + canvas = self.canvas, + data = self._reach, + toolbar = None, + display_current = False + ) + self.plot.draw() def setup_connections(self): From edc99230d7ecccb07f9afd9c142cc22939fdbd2a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 12:08:42 +0200 Subject: [PATCH 12/34] SL: Add cmd apply for reach. --- src/View/SedimentLayers/Reach/SLDialog.py | 58 +++++++++++++++++ src/View/SedimentLayers/Reach/Table.py | 8 +++ src/View/SedimentLayers/Reach/UndoCommand.py | 19 ++++++ src/View/SedimentLayers/Reach/Window.py | 25 ++++++++ src/View/ui/ReachSedimentLayers.ui | 4 +- src/View/ui/SLDialog.ui | 67 ++++++++++++++++++++ 6 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/View/SedimentLayers/Reach/SLDialog.py create mode 100644 src/View/ui/SLDialog.ui diff --git a/src/View/SedimentLayers/Reach/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py new file mode 100644 index 00000000..ed4d3248 --- /dev/null +++ b/src/View/SedimentLayers/Reach/SLDialog.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +from View.ASubWindow import ASubWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QComboBox, QUndoStack, QShortcut, + QDoubleSpinBox, +) + +from View.SedimentLayers.Reach.translate import * + +_translate = QCoreApplication.translate + +class SLDialog(ASubWindow, ListedSubWindow): + def __init__(self, title="SL", study=None, parent=None): + self._study = study + + super(SLDialog, self).__init__( + name=title, ui="SLDialog", parent=parent + ) + + self.setup_combobox() + + self.value = None + + def setup_combobox(self): + self.combobox_add_items( + "comboBox", + [_translate("SedimentLayers", "Not defined")] + + list( + map( + lambda sl: str(sl), + self._study.river.sediment_layers.sediment_layers + ) + ) + ) + + @property + def sl(self): + return next( + filter( + lambda sl: str(sl) == self.value, + self._study.river.sediment_layers.sediment_layers + ) + ) + + def accept(self): + self.value = self.get_combobox_text("comboBox") + super().accept() diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py index d9cac095..8fdb8d7e 100644 --- a/src/View/SedimentLayers/Reach/Table.py +++ b/src/View/SedimentLayers/Reach/Table.py @@ -142,6 +142,14 @@ class TableModel(QAbstractTableModel): self.dataChanged.emit(index, index) return True + def apply_sl_each_profile(self, sl): + self._undo.push( + ApplySLCommand( + self._reach, sl + ) + ) + self.layoutChanged.emit() + def undo(self): self._undo.undo() self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/Reach/UndoCommand.py b/src/View/SedimentLayers/Reach/UndoCommand.py index 82ba3764..17e1f0f1 100644 --- a/src/View/SedimentLayers/Reach/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/UndoCommand.py @@ -30,3 +30,22 @@ class SetSLCommand(QUndoCommand): def redo(self): self._reach.profile(self._index).sl = self._new + + +class ApplySLCommand(QUndoCommand): + def __init__(self, reach, new_value): + QUndoCommand.__init__(self) + + self._reach = reach + self._old = [] + for profile in self._reach.profiles: + self._old.append(profile.sl) + self._new = new_value + + def undo(self): + for i, profile in enumerate(self._reach.profiles): + profile.sl = self._old[i] + + def redo(self): + for profile in self._reach.profiles: + profile.sl = self._new diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 4d155cec..01f36348 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -27,6 +27,7 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.Reach.UndoCommand import * from View.SedimentLayers.Reach.Table import * from View.SedimentLayers.Reach.Plot import Plot +from View.SedimentLayers.Reach.SLDialog import SLDialog from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.Reach.translate import * @@ -111,6 +112,13 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def setup_connections(self): self.find(QAction, "action_edit").triggered.connect(self.edit_profile) + self.find(QPushButton, "pushButton_edit")\ + .clicked\ + .connect(self.edit_sl) + self.find(QPushButton, "pushButton_apply")\ + .clicked\ + .connect(self.apply_sl_each_profile) + self.undo_sc.activated.connect(self.undo) self.redo_sc.activated.connect(self.redo) self.copy_sc.activated.connect(self.copy) @@ -140,6 +148,15 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def redo(self): self._table.redo() + def apply_sl_each_profile(self): + slw = SLDialog( + study = self._study, + parent = self + ) + if slw.exec(): + sl = slw.sl + self._table.apply_sl_each_profile(sl) + def edit_profile(self): rows = self.index_selected_rows() @@ -150,3 +167,11 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): parent = self ) slw.show() + + + def edit_sl(self): + slw = SedimentLayersWindow( + study = self._study, + parent = self + ) + slw.show() diff --git a/src/View/ui/ReachSedimentLayers.ui b/src/View/ui/ReachSedimentLayers.ui index a1beb8ca..dfe39a82 100644 --- a/src/View/ui/ReachSedimentLayers.ui +++ b/src/View/ui/ReachSedimentLayers.ui @@ -26,14 +26,14 @@ - + Edit sediment layers list - + Apply sediment layers on all reach diff --git a/src/View/ui/SLDialog.ui b/src/View/ui/SLDialog.ui new file mode 100644 index 00000000..8681418b --- /dev/null +++ b/src/View/ui/SLDialog.ui @@ -0,0 +1,67 @@ + + + Dialog + + + + 0 + 0 + 194 + 76 + + + + Dialog + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 043d8d839a9b4a79032761609dbbc0a6a1fb273a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 13:28:47 +0200 Subject: [PATCH 13/34] SL: Reach: Profile: Add Plot. --- src/Model/Geometry/Profile.py | 33 +++++++ src/View/SedimentLayers/Reach/Plot.py | 1 + src/View/SedimentLayers/Reach/Profile/Plot.py | 97 +++++++++++++++++++ .../SedimentLayers/Reach/Profile/Window.py | 15 +-- 4 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 src/View/SedimentLayers/Reach/Profile/Plot.py diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 483082b0..7199730f 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -275,6 +275,39 @@ class Profile(object): ) self._status.modified() + # Sediment Layers + + def get_sl(self): + """Get sediment layer height of points + + Get sediment layer of points (without spesific point sl) + + Returns: + List of sediment layers height + """ + res = [] + psl = [point.sl for point in self.points] + + # Compute max number of layers + sl_max = 0 + for sl in psl: + n = 0 if sl is None else len(sl) + sl_max = max(n, sl_max) + + # Create list of height for each sl and each layer + for i in range(0, sl_max): + cur = [] + # Compute new layer line for each sl + for sl in psl: + if sl is not None and i < len(sl): + cur.append(sl.get(i).height) + else: + cur.append(0) + # Add layer line to result + res.append(cur) + + return res + # Abstract method, must be implemented for in non abstract class def get_station(self): raise NotImplementedMethodeError(self, self.get_station) diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py index af7a54ed..1ab6daba 100644 --- a/src/View/SedimentLayers/Reach/Plot.py +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -58,6 +58,7 @@ class Plot(APlot): left = min(kp), right = max(kp) ) + # Compute sediment layer in function to profile z_min z_sl = reduce( lambda acc, v: acc + [ list( diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py new file mode 100644 index 00000000..b8c9b66f --- /dev/null +++ b/src/View/SedimentLayers/Reach/Profile/Plot.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class Plot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None, + display_current=True): + super(Plot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + self._display_current = display_current + + self.line_kp_zmin = None + self.line_kp_sl = [] + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + if self.data.number_points == 0: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + x = self.data.get_station() + z = self.data.z() + sl = self.data.get_sl() + + self.canvas.axes.set_xlim( + left = min(x), right = max(x) + ) + + # Compute sediment layer in function to point z + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl, + [z] + ) + + for i, zsl in enumerate(z_sl): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + x, zsl, + linestyle="solid" if i == 0 else "--", + lw=1.8, + color='grey' if i == 0 else None + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + if ind is None: + logger.info("TODO: Update") + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index 8f15a27e..a622bfb5 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.Reach.Profile.UndoCommand import * from View.SedimentLayers.Reach.Profile.Table import * +from View.SedimentLayers.Reach.Profile.Plot import Plot from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.Reach.Profile.translate import * @@ -108,13 +109,13 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # display_current = False - # ) - # self.plot.draw() + self.plot = Plot( + canvas = self.canvas, + data = self._profile, + toolbar = None, + display_current = False + ) + self.plot.draw() def setup_connections(self): From 181776f33fef562ffc57cab63019b85a6fe6f41d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 13:51:49 +0200 Subject: [PATCH 14/34] SL: Edit: Add Plot. --- src/Model/SedimentLayer/SedimentLayer.py | 14 +++ src/View/SedimentLayers/Edit/Plot.py | 104 +++++++++++++++++++++++ src/View/SedimentLayers/Edit/Window.py | 15 ++-- 3 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/View/SedimentLayers/Edit/Plot.py diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index f1b8a88a..020f247a 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -143,6 +143,15 @@ class SedimentLayer(SQLSubModel): def __len__(self): return len(self._layers) + @property + def layers(self): + return self._layers.copy() + + def height(self): + return list( + map(lambda l: l.height, self._layers) + ) + @property def name(self): return self._name @@ -151,6 +160,11 @@ class SedimentLayer(SQLSubModel): def name(self, name): self._name = name + def names(self): + return list( + map(lambda l: l.name, self._layers) + ) + @property def comment(self): return self._comment diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py new file mode 100644 index 00000000..86a1c8d9 --- /dev/null +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class Plot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None, + display_current=True): + super(Plot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + self._display_current = display_current + + self.line_kp_zmin = None + self.line_kp_sl = [] + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + x = [0,1] + z = [0,0] + sl = self.data.height() + names = ["fond"] + self.data.names() + + self.canvas.axes.set_xlim( + left = min(x), right = max(x) + ) + + lsl = [] + for i in range(len(sl)): + cur = [] + for _ in x: + cur.append(sl[i]) + lsl.append(cur) + + # Compute sediment layer in function to point z + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + lsl, + [z] + ) + + for i, zsl in enumerate(z_sl): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + x, zsl, + label=names[i], + linestyle="solid" if i == 0 else "--", + lw=1.8, + color='grey' if i == 0 else None + ) + self.canvas.axes.text(x[0] + 0.01, zsl[0] + 0.01, f'{names[i]}') + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + if ind is None: + logger.info("TODO: Update") + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index 44100d6f..a95e4ff0 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -28,6 +28,7 @@ from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.Edit.UndoCommand import * from View.SedimentLayers.Edit.Table import * +from View.SedimentLayers.Edit.Plot import Plot from View.SedimentLayers.Edit.translate import * _translate = QCoreApplication.translate @@ -91,13 +92,13 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # display_current = False - # ) - # self.plot.draw() + self.plot = Plot( + canvas = self.canvas, + data = self._sl, + toolbar = None, + display_current = False + ) + self.plot.draw() def setup_connections(self): From c5402ebb555c9db57578a9cc3bcf705a439d873a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 13:56:16 +0200 Subject: [PATCH 15/34] SL: Edit: Minor change. --- src/View/SedimentLayers/Edit/Plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index 86a1c8d9..14ebb948 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -49,7 +49,7 @@ class Plot(APlot): x = [0,1] z = [0,0] sl = self.data.height() - names = ["fond"] + self.data.names() + names = ["bottom"] + self.data.names() self.canvas.axes.set_xlim( left = min(x), right = max(x) From b87977a2d8572c11b6792543ff1b317e11ee2f10 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 14:22:50 +0200 Subject: [PATCH 16/34] SL: Save sl fk in database for point and profile. --- src/Model/Geometry/PointXYZ.py | 30 +++++++++++++++++++++++++++--- src/Model/Geometry/Profile.py | 2 +- src/Model/Geometry/ProfileXYZ.py | 30 +++++++++++++++++++++++++++--- src/Model/River.py | 14 ++++++++------ src/Model/Study.py | 2 +- 5 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 4e55c7b8..ff0a62fb 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -37,6 +37,17 @@ class PointXYZ(Point, SQLSubModel): @classmethod def _sql_update(cls, execute, version): cls._update_submodel(execute, version) + + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + execute( + """ + ALTER TABLE geometry_pointXYZ + ADD COLUMN sl INTEGER + REFERENCES sedimentary_layer(id) + """ + ) return True @classmethod @@ -47,7 +58,7 @@ class PointXYZ(Point, SQLSubModel): profile = data["profile"] table = execute( - "SELECT ind, name, x, y, z " + + "SELECT ind, name, x, y, z, sl " + "FROM geometry_pointXYZ " + f"WHERE profile = {profile.id}" ) @@ -63,6 +74,7 @@ class PointXYZ(Point, SQLSubModel): x = row[2] y = row[3] z = row[4] + sl = row[5] new = cls( name = name, @@ -71,6 +83,16 @@ class PointXYZ(Point, SQLSubModel): status = status ) + if sl == -1 or sl == None: + new._sl = None + else: + new._sl = next( + filter( + lambda s: s.id == sl, + data["sediment_layers_list"].sediment_layers + ) + ) + points[ind] = new return points @@ -79,13 +101,15 @@ class PointXYZ(Point, SQLSubModel): profile = data["profile"] ind = data["ind"] + sl = self._sl.id if self._sl is not None else -1 + sql = ( "INSERT OR REPLACE INTO " + - "geometry_pointXYZ(ind, name, x, y, z, profile) "+ + "geometry_pointXYZ(ind, name, x, y, z, profile, sl) "+ "VALUES (" + f"{ind}, '{self._sql_format(self._name)}', " + f"{self.x}, {self.y}, {self.z}, " + - f"{profile.id}" + + f"{profile.id}, {sl}" + ")" ) execute(sql) diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 7199730f..9fa8e9ed 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -133,7 +133,7 @@ class Profile(object): return self._sl @sl.setter - def sl(self, value: str): + def sl(self, value): self._sl = value self._status.modified() diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index af310769..cb681383 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -68,6 +68,17 @@ class ProfileXYZ(Profile, SQLSubModel): @classmethod def _sql_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + execute( + """ + ALTER TABLE geometry_profileXYZ + ADD COLUMN sl INTEGER + REFERENCES sedimentary_layer(id) + """ + ) + return cls._update_submodel(execute, version) @classmethod @@ -77,7 +88,7 @@ class ProfileXYZ(Profile, SQLSubModel): reach = data["reach"] table = execute( - "SELECT id, ind, name, kp, num, code1, code2 " + + "SELECT id, ind, name, kp, num, code1, code2, sl " + "FROM geometry_profileXYZ " + f"WHERE reach = {reach}" ) @@ -93,6 +104,7 @@ class ProfileXYZ(Profile, SQLSubModel): num = row[5] code1 = row[5] code2 = row[6] + sl = row[7] new = cls( id=id, num = num, @@ -102,6 +114,16 @@ class ProfileXYZ(Profile, SQLSubModel): status = status ) + if sl == -1 or sl == None: + new._sl = None + else: + new._sl = next( + filter( + lambda s: s.id == sl, + data["sediment_layers_list"].sediment_layers + ) + ) + data["profile"] = new new._points = PointXYZ._sql_load(execute, data) @@ -113,13 +135,15 @@ class ProfileXYZ(Profile, SQLSubModel): ok = True ind = data["ind"] + sl = self._sl.id if self._sl is not None else -1 + sql = ( "INSERT OR REPLACE INTO " + - "geometry_profileXYZ(id, ind, name, reach, kp, num, code1, code2) "+ + "geometry_profileXYZ(id, ind, name, reach, kp, num, code1, code2, sl) "+ "VALUES (" + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + f"{self.reach.id}, {self.kp}, {self.num}, " + - f"{self.code1}, {self.code1}" + + f"{self.code1}, {self.code1}, {sl}" + ")" ) execute(sql) diff --git a/src/Model/River.py b/src/Model/River.py index 59b47957..b21a95a9 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -233,6 +233,14 @@ class River(Graph, SQLSubModel): ) data["stricklers"] = new._stricklers + + # Initial conditions + new._sediment_layers = SedimentLayerList._sql_load( + execute, + data + ) + data["sediment_layers_list"] = new._sediment_layers + # Network new._nodes = RiverNode._sql_load( execute, @@ -263,12 +271,6 @@ class River(Graph, SQLSubModel): data ) - # Initial conditions - new._sediment_layers = SedimentLayerList._sql_load( - execute, - data - ) - # Parameters new._parameters = SolverParametersList._sql_load( execute, diff --git a/src/Model/Study.py b/src/Model/Study.py index bdebce47..fc553f94 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -24,7 +24,7 @@ class Study(SQLModel): def __init__(self, filename = None, init_new = True): # Metadata - self._version = "0.0.1" + self._version = "0.0.2" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() From 9e0fb53b604aeb0574abaccd933129d5d8b84502 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 14:48:54 +0200 Subject: [PATCH 17/34] SL: Fix database update. --- src/Model/SedimentLayer/SedimentLayer.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 020f247a..a18b88fb 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -70,6 +70,11 @@ class Layer(SQLSubModel): @classmethod def _sql_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + cls._sql_create(execute) + return True @classmethod @@ -187,6 +192,11 @@ class SedimentLayer(SQLSubModel): @classmethod def _sql_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + cls._sql_create(execute) + return True @classmethod From ef398c78b1313d9deac2c19b7676d4cbf8cc6cea Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 15:06:07 +0200 Subject: [PATCH 18/34] Debug: Add SqliteBrowser debug action. --- src/View/MainWindow.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 2c9b1b65..71301e1b 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -2,6 +2,7 @@ import os import logging +import subprocess from queue import Queue from PyQt5 import QtGui @@ -232,15 +233,22 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.debug_action.setStatusTip("Debug") self.debug_action.triggered.connect(self.open_debug) + self.debug_sqlite_action = QAction("Debug SQLite", self) + self.debug_sqlite_action.setStatusTip("Open SQLite debuging tool (sqlitebrowser)") + self.debug_sqlite_action.triggered.connect(self.open_sqlite) + if self.conf.debug: menu.addAction(self.debug_action) + menu.addAction(self.debug_sqlite_action) self.set_debug_lvl(debug = True) else: if self.conf.debug: menu.addAction(self.debug_action) + menu.addAction(self.debug_sqlite_action) self.set_debug_lvl(debug = True) else: menu.removeAction(self.debug_action) + menu.removeAction(self.debug_sqlite_action) self.set_debug_lvl(debug = False) ######### @@ -549,6 +557,17 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) repl.show() + def open_sqlite(self): + if self.model is None: + logger.debug("No study open for sql debuging...") + return + + file = self.model.filename + _ = subprocess.Popen( + f"sqlitebrowser {file}", + shell=True + ) + # TODO: Delete me ! ############### # DUMMY STUFF # From b0810f6eed640434416a25ddf1e8398b3b635808 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 15:43:02 +0200 Subject: [PATCH 19/34] SL: Fix move on edit window and some minor fix. --- src/Model/SedimentLayer/SedimentLayer.py | 2 ++ src/View/SedimentLayers/Edit/Table.py | 6 ++--- src/View/SedimentLayers/Edit/Window.py | 23 ++++++++++++++-- src/View/SedimentLayers/Window.py | 34 +++++++++++++++++------- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index a18b88fb..20af62fd 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -284,6 +284,7 @@ class SedimentLayer(SQLSubModel): l = self._layers l[index], l[next] = l[next], l[index] + self._status.modified() def move_down(self, index): @@ -292,4 +293,5 @@ class SedimentLayer(SQLSubModel): l = self._layers l[index], l[prev] = l[prev], l[index] + self._status.modified() diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index 32790f1b..47a87009 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -124,7 +124,7 @@ class TableModel(QAbstractTableModel): self.beginMoveRows(parent, row - 1, row - 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( self._sl, "up", row ) @@ -133,7 +133,7 @@ class TableModel(QAbstractTableModel): self.endMoveRows() self.layoutChanged.emit() - def move_down(self, index, parent=QModelIndex()): + def move_down(self, row, parent=QModelIndex()): if row > len(self._sl): return @@ -141,7 +141,7 @@ class TableModel(QAbstractTableModel): self.beginMoveRows(parent, row + 1, row + 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( self._sl, "down", row ) diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index a95e4ff0..965895d2 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -91,7 +91,9 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.canvas.setObjectName("canvas") self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) + self._set_plot() + def _set_plot(self): self.plot = Plot( canvas = self.canvas, data = self._sl, @@ -104,14 +106,17 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): 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_move_up").triggered.connect(self.delete) - self.find(QAction, "action_move_down").triggered.connect(self.delete) + self.find(QAction, "action_move_up").triggered.connect(self.move_up) + self.find(QAction, "action_move_down").triggered.connect(self.move_down) 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._set_plot) + self._table.layoutChanged.connect(self._set_plot) + def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( @@ -138,6 +143,20 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self._table.delete(rows) + def move_up(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.move_up(rows[0]) + + def move_down(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.move_down(rows[0]) + def copy(self): logger.info("TODO: copy") diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index b04c073d..f8b9d2d2 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -27,6 +27,8 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.UndoCommand import * from View.SedimentLayers.Table import * +from View.SedimentLayers.Edit.Plot import Plot + from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.translate import * @@ -85,15 +87,6 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # display_current = False - # ) - # 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) @@ -104,6 +97,29 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.copy_sc.activated.connect(self.copy) self.paste_sc.activated.connect(self.paste) + table = self.find(QTableView, f"tableView") + table.selectionModel()\ + .selectionChanged\ + .connect(self._set_current_sl) + + self._table.dataChanged\ + .connect(self._set_current_sl) + + def _set_current_sl(self): + rows = self.index_selected_rows() + + if len(rows) == 0: + return + + self.plot = Plot( + canvas = self.canvas, + data = self._sediment_layers.get(rows[0]), + toolbar = None, + display_current = False + ) + self.plot.draw() + + def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( From bccbc41fe822479f203902c337022ed69a227a65 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 16:53:37 +0200 Subject: [PATCH 20/34] SL: Minor fixes. --- src/Model/SedimentLayer/SedimentLayer.py | 7 ++++++- src/View/SedimentLayers/Reach/SLDialog.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 20af62fd..c4e92621 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -143,7 +143,12 @@ class SedimentLayer(SQLSubModel): SedimentLayer._id_cnt = max(id, SedimentLayer._id_cnt+1) def __str__(self): - return f"{self.name} ({len(self)}) - {self.comment}" + s = f"{self.name} ({len(self)})" + + if self.comment != "": + s += f" - {self.comment}" + + return s def __len__(self): return len(self._layers) diff --git a/src/View/SedimentLayers/Reach/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py index ed4d3248..ce1e9794 100644 --- a/src/View/SedimentLayers/Reach/SLDialog.py +++ b/src/View/SedimentLayers/Reach/SLDialog.py @@ -46,6 +46,9 @@ class SLDialog(ASubWindow, ListedSubWindow): @property def sl(self): + if self.value == _translate("SedimentLayers", "Not defined"): + return None + return next( filter( lambda sl: str(sl) == self.value, From a9b905a03957e8dfd37767f4be785213d7902c98 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 12:15:16 +0200 Subject: [PATCH 21/34] SL: Reach/Profile: Add update. --- src/View/SedimentLayers/Reach/Profile/Window.py | 9 +++++++++ src/View/SedimentLayers/Reach/Window.py | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index a622bfb5..93b201a6 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -109,6 +109,9 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) + self._update_plot() + + def _update_plot(self): self.plot = Plot( canvas = self.canvas, data = self._profile, @@ -124,6 +127,12 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.copy_sc.activated.connect(self.copy) self.paste_sc.activated.connect(self.paste) + self._table.layoutChanged\ + .connect(self._update_plot) + self._table.dataChanged\ + .connect(self._update_plot) + + def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 01f36348..ee477edc 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -100,6 +100,9 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout_2") self.plot_layout.addWidget(self.canvas) + self._update_plot() + + def _update_plot(self): self.plot = Plot( canvas = self.canvas, data = self._reach, @@ -124,6 +127,12 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.copy_sc.activated.connect(self.copy) self.paste_sc.activated.connect(self.paste) + self._table.layoutChanged\ + .connect(self._update_plot) + self._table.dataChanged\ + .connect(self._update_plot) + + def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( @@ -144,9 +153,11 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def undo(self): self._table.undo() + self._update_plot() def redo(self): self._table.redo() + self._update_plot() def apply_sl_each_profile(self): slw = SLDialog( @@ -157,6 +168,8 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): sl = slw.sl self._table.apply_sl_each_profile(sl) + self._update_plot() + def edit_profile(self): rows = self.index_selected_rows() From 003be29ecac20afa5fd58956b1b997b0be3415fe Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 13:26:41 +0200 Subject: [PATCH 22/34] SL: Fix move down exception. --- src/Model/SedimentLayer/SedimentLayer.py | 4 ++-- src/View/SedimentLayers/Edit/Table.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index c4e92621..1bbcb602 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -284,7 +284,7 @@ class SedimentLayer(SQLSubModel): self.delete(els) def move_up(self, index): - if index < len(self._layers): + if index >= 0: next = index - 1 l = self._layers @@ -293,7 +293,7 @@ class SedimentLayer(SQLSubModel): self._status.modified() def move_down(self, index): - if index >= 0: + if index + 1 < len(self._layers): prev = index + 1 l = self._layers diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index 47a87009..91a13b3f 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -134,7 +134,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, row, parent=QModelIndex()): - if row > len(self._sl): + if row + 1 >= len(self._sl): return target = row From 02ef127942c9d4ebbcaad4c1bc66b0e76bbb8a7f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 15:11:44 +0200 Subject: [PATCH 23/34] SL: Reach: Profile: Minor change. --- src/View/SedimentLayers/Reach/Profile/Table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py index 2675e8ee..102bec32 100644 --- a/src/View/SedimentLayers/Reach/Profile/Table.py +++ b/src/View/SedimentLayers/Reach/Profile/Table.py @@ -34,7 +34,7 @@ class ComboBoxDelegate(QItemDelegate): self.editor = QComboBox(parent) self.editor.addItems( - [_translate("SedimentLayers", "Not defined")] + + # [_translate("SedimentLayers", "Not defined")] + list( map( lambda sl: str(sl), From d636f5b637c0f1b97c30660cb257cac0839eb513 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 28 Jul 2023 11:48:00 +0200 Subject: [PATCH 24/34] SL: Reach: Profile: Add KP when profile has no name. --- src/View/SedimentLayers/Reach/Profile/Window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index 93b201a6..d1dd5e69 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -64,7 +64,10 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): pname = self._profile.name if pname == "": - pname = _translate("SedimentLayers", "(no name)") + pname = _translate( + "SedimentLayers", + "(no name - @kp)").replace("@kp", str(self._profile.kp) + ) self._title = ( title + " - " From 17d4b157610a4f44761d5137b9e41c9cdcf68476 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 13:40:46 +0200 Subject: [PATCH 25/34] SedimentLayer: Add d50, sigma and critical_constraint data. --- src/Model/SedimentLayer/SedimentLayer.py | 45 +++++++++++++++++++-- src/View/SedimentLayers/Edit/Table.py | 24 +++++++++++ src/View/SedimentLayers/Edit/UndoCommand.py | 45 +++++++++++++++++++++ src/View/SedimentLayers/Edit/translate.py | 3 ++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 1bbcb602..638212c8 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -11,7 +11,9 @@ class Layer(SQLSubModel): def __init__(self, id:int = -1, name:str = "", - type = "", height = 0, + type = "", + height = 0.0, d50 = 0.0, sigma = 0.0, + critical_constraint = 0.0, sl = None, status = None): super(Layer, self).__init__() @@ -19,7 +21,11 @@ class Layer(SQLSubModel): self._name = name self._type = type + self._height = height + self._d50 = d50 + self._sigma = sigma + self._critical_constraint = critical_constraint if id == -1: self.id = Layer._id_cnt @@ -52,6 +58,30 @@ class Layer(SQLSubModel): def height(self, height): self._height = float(height) + @property + def d50(self): + return self._d50 + + @d50.setter + def d50(self, d50): + self._d50 = float(d50) + + @property + def sigma(self): + return self._sigma + + @sigma.setter + def sigma(self, sigma): + self._sigma = float(sigma) + + @property + def critical_constraint(self): + return self._critical_constraint + + @critical_constraint.setter + def critical_constraint(self, critical_constraint): + self._critical_constraint = float(critical_constraint) + @classmethod def _sql_create(cls, execute): execute(""" @@ -61,6 +91,9 @@ class Layer(SQLSubModel): name TEXT NOT NULL, type TEXT NOT NULL, height REAL NOT NULL, + d50 REAL NOT NULL, + sigma REAL NOT NULL, + critical_constraint REAL NOT NULL, sl INTEGER, FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) ) @@ -83,7 +116,7 @@ class Layer(SQLSubModel): sl = data["sl"] table = execute( - "SELECT id, ind, name, type, height " + + "SELECT id, ind, name, type, height, d50, sigma, critical_constraint " + "FROM sedimentary_layer_layer " + f"WHERE sl = {sl}" ) @@ -97,6 +130,8 @@ class Layer(SQLSubModel): layer = cls( id = row[0], name = row[2], type = row[3], height = row[4], + d50 = row[5], sigma = row[6], + critical_constraint = row[7], sl = sl, status = data['status'] ) @@ -110,10 +145,12 @@ class Layer(SQLSubModel): sql = ( "INSERT INTO " + - "sedimentary_layer_layer(id, ind, name, type, height, sl) "+ + "sedimentary_layer_layer(id, ind, name, type, height, d50, sigma, critical_constraint, sl) "+ "VALUES (" + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._type)}', '{self._height}', {sl.id}" + + f"'{self._sql_format(self._type)}', {self._height}, " + + f"{self._d50}, {self._sigma}, {self._critical_constraint}, " + + f"{sl.id}" + ")" ) execute(sql) diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index 91a13b3f..e1a41cb2 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -54,6 +54,12 @@ class TableModel(QAbstractTableModel): return self._sl.get(row).type elif self._headers[column] == "height": return self._sl.get(row).height + elif self._headers[column] == "d50": + return self._sl.get(row).d50 + elif self._headers[column] == "sigma": + return self._sl.get(row).sigma + elif self._headers[column] == "critical_constraint": + return self._sl.get(row).critical_constraint return QVariant() @@ -88,6 +94,24 @@ class TableModel(QAbstractTableModel): self._sl, row, value ) ) + if self._headers[column] == "d50": + self._undo.push( + SetD50Command( + self._sl, row, value + ) + ) + if self._headers[column] == "sigma": + self._undo.push( + SetSigmaCommand( + self._sl, row, value + ) + ) + if self._headers[column] == "critical_constraint": + self._undo.push( + SetCriticalConstraintCommand( + self._sl, row, value + ) + ) self.dataChanged.emit(index, index) return True diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py index e6964709..298fa529 100644 --- a/src/View/SedimentLayers/Edit/UndoCommand.py +++ b/src/View/SedimentLayers/Edit/UndoCommand.py @@ -55,6 +55,51 @@ class SetHeightCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).height = self._new +class SetD50Command(QUndoCommand): + def __init__(self, sediment_layers, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).d50 + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).d50 = self._old + + def redo(self): + self._sediment_layers.get(self._index).d50 = self._new + +class SetSigmaCommand(QUndoCommand): + def __init__(self, sediment_layers, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).sigma + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).sigma = self._old + + def redo(self): + self._sediment_layers.get(self._index).sigma = self._new + +class SetCriticalConstraintCommand(QUndoCommand): + def __init__(self, sediment_layers, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).critical_constraint + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).critical_constraint = self._old + + def redo(self): + self._sediment_layers.get(self._index).critical_constraint = self._new + class AddCommand(QUndoCommand): def __init__(self, sediment_layers, index): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py index 3b61d18c..fe9e44a9 100644 --- a/src/View/SedimentLayers/Edit/translate.py +++ b/src/View/SedimentLayers/Edit/translate.py @@ -8,4 +8,7 @@ table_headers = { "name": _translate("SedimentLayers", "Name"), "type": _translate("SedimentLayers", "Type"), "height": _translate("Sedimentlayers", "Height"), + "d50": _translate("Sedimentlayers", "D50"), + "sigma": _translate("Sedimentlayers", "Sigma"), + "critical_constraint": _translate("Sedimentlayers", "Critical constraint"), } From 47ec0ac9b50683098ea1ff1043daf18fc1518473 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 14:06:35 +0200 Subject: [PATCH 26/34] SL: Plots reverse draw order. --- src/View/SedimentLayers/Edit/Plot.py | 10 +++++----- src/View/SedimentLayers/Reach/Plot.py | 6 +++--- src/View/SedimentLayers/Reach/Profile/Plot.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index 14ebb948..8f25c436 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -73,16 +73,16 @@ class Plot(APlot): [z] ) - for i, zsl in enumerate(z_sl): + for i, zsl in enumerate(reversed(z_sl)): self.line_kp_sl.append(None) self.line_kp_sl[i], = self.canvas.axes.plot( x, zsl, - label=names[i], - linestyle="solid" if i == 0 else "--", + label=names[-(i+1)], + linestyle="solid" if i == len(names) - 1 else "--", lw=1.8, - color='grey' if i == 0 else None + color='grey' if i == len(names) - 1 else None ) - self.canvas.axes.text(x[0] + 0.01, zsl[0] + 0.01, f'{names[i]}') + self.canvas.axes.text(x[0] + 0.01, zsl[0] + 0.01, f'{names[-(i+1)]}') self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py index 1ab6daba..a312a8ce 100644 --- a/src/View/SedimentLayers/Reach/Plot.py +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -69,13 +69,13 @@ class Plot(APlot): [z_min] ) - for i, z in enumerate(z_sl): + for i, z in enumerate(reversed(z_sl)): self.line_kp_sl.append(None) self.line_kp_sl[i], = self.canvas.axes.plot( kp, z, - linestyle="solid" if i == 0 else "--", + linestyle="solid" if i == len(z_sl) - 1 else "--", lw=1.8, - color='grey' if i == 0 else None + color='grey' if i == len(z_sl) - 1 else None ) self.canvas.figure.tight_layout() diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py index b8c9b66f..50b4bba2 100644 --- a/src/View/SedimentLayers/Reach/Profile/Plot.py +++ b/src/View/SedimentLayers/Reach/Profile/Plot.py @@ -68,13 +68,13 @@ class Plot(APlot): [z] ) - for i, zsl in enumerate(z_sl): + for i, zsl in enumerate(reversed(z_sl)): self.line_kp_sl.append(None) self.line_kp_sl[i], = self.canvas.axes.plot( x, zsl, - linestyle="solid" if i == 0 else "--", + linestyle="solid" if i == len(z_sl) - 1 else "--", lw=1.8, - color='grey' if i == 0 else None + color='grey' if i == len(z_sl) - 1 else None ) self.canvas.figure.tight_layout() From e199ac7d5ef4defd1a86bf12ae0d80776919e299 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 15:02:43 +0200 Subject: [PATCH 27/34] SL: SL edit disable X axis. --- src/View/SedimentLayers/Edit/Plot.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index 8f25c436..e48ec5eb 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -34,18 +34,19 @@ class Plot(APlot): self.canvas.axes.cla() self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) - if self.data is None: - return - - self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "X (m)"), - color='green', fontsize=12 - ) + # self.canvas.axes.set_xlabel( + # _translate("MainWindow_reach", "X (m)"), + # color='green', fontsize=12 + # ) + self.canvas.axes.axes.get_xaxis().set_visible(False) self.canvas.axes.set_ylabel( _translate("MainWindow_reach", "Height (m)"), color='green', fontsize=12 ) + if self.data is None: + return + x = [0,1] z = [0,0] sl = self.data.height() From 768a0b9f14ee6d865b9e4811b05d92f02489c63f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 11 Aug 2023 15:46:09 +0200 Subject: [PATCH 28/34] Sediment: Add solid BC, QSO export and profile + point sediment layers export. --- .../BoundaryCondition/BoundaryCondition.py | 5 +- .../BoundaryConditionList.py | 3 +- .../BoundaryConditionTypes.py | 13 +++ src/Solver/ASolver.py | 14 +++- src/Solver/Mage.py | 76 ++++++++++++++++-- src/View/BoundaryCondition/Edit/translate.py | 3 +- src/View/BoundaryCondition/translate.py | 7 +- tests_cases/Saar/Saar.pamhyr | Bin 176128 -> 192512 bytes 8 files changed, 107 insertions(+), 14 deletions(-) diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index 86e0f1d2..d6abb6c9 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -83,7 +83,8 @@ class BoundaryCondition(SQLSubModel): def _get_ctor_from_type(cls, t): from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, - TimeOverZ, TimeOverDischarge, ZOverDischarge + TimeOverZ, TimeOverDischarge, ZOverDischarge, + Solid, ) res = NotDefined @@ -95,6 +96,8 @@ class BoundaryCondition(SQLSubModel): res = TimeOverDischarge elif t == "ZD": res = ZOverDischarge + elif t == "SL": + res = Solid return res @classmethod diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py index 1a342dc7..2514f326 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionList.py +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -26,7 +26,8 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, - TimeOverZ, TimeOverDischarge, ZOverDischarge + TimeOverZ, TimeOverDischarge, ZOverDischarge, + Solid, ) class BoundaryConditionList(SQLSubModel): diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py index b2c73f11..963e88e7 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -83,3 +83,16 @@ class ZOverDischarge(BoundaryCondition): @property def _default_0(self): return 0.0 + + +class Solid(BoundaryCondition): + def __init__(self, id:int = -1, name:str = "", status = None): + super(Solid, self).__init__(id=id, name=name, status=status) + + self._type = "SL" + self._header = ["time", "solid"] + self._types = [TimeOverDischarge.time_convert, float] + + @classmethod + def compatibility(cls): + return ["solid"] diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index 64cc01cb..ea268999 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -213,12 +213,22 @@ class AbstractSolver(object): # Command line executable path is between " char cmd = cmd.split("\"") exe = cmd[1].replace("\ ", " ") - args = "\"".join(cmd[2:]).split(" ")[1:] + args = list( + filter( + lambda s: s != "", + "\"".join(cmd[2:]).split(" ")[1:] + ) + ) else: # We suppose the command line executable path as no space char cmd = cmd.replace("\ ", "&_&").split(" ") exe = cmd[0].replace("&_&", " ") - args = list(map(lambda s: s.replace("&_&", "\ "), cmd[1:])) + args = list( + filter( + lambda s: s != "", + map(lambda s: s.replace("&_&", "\ "), cmd[1:]) + ) + ) logger.info(f"! {exe} {args}") return exe, args diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 6c129f88..81f99722 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -127,23 +127,47 @@ class Mage(AbstractSolver): with mage_file_open(os.path.join(repertory, f"{name}.ST"), "w+") as f: files.append(f"{name}.ST") + cnt_num = 1 for profile in edge.reach.profiles: - num = f"{profile.num:>6}" + num = f"{cnt_num:>6}" c1 = f"{profile.code1:>6}" c2 = f"{profile.code2:>6}" t = f"{len(profile.points):>6}" kp = f"{profile.kp:>13.4f}" - name = profile.name + pname = profile.name + if profile.name == "": + pname = f"p{profile.id:>3}".replace(" ", "p") + name = f"{pname}" - f.write(f"{num}{c1}{c2}{t}{kp} {name}\n") + sediment = "" + if profile.sl is not None: + if not any(filter(lambda f: ".GRA" in f, files)): + files.append("0.GRA") + + nl = len(profile.sl) + sediment = f" {nl:>3}" + for l in profile.sl.layers: + sediment += f" {l.height:>10} {l.d50:>10} {l.sigma:>10} {l.critical_constraint:>10}" + + f.write(f"{num}{c1}{c2}{t}{kp} {name} {sediment}\n") + cnt_num += 1 for point in profile.points: x = f"{point.x:>13.4f}" y = f"{point.y:>13.4f}" z = f"{point.z:>13.4f}" - n = point.name + n = f"{point.name:<3}" - f.write(f"{x}{y}{z} {n}\n") + sediment = "" + prev = point.z + if point.sl is not None: + nl = len(point.sl) + sediment = f"{nl:>3}" + for l in point.sl.layers: + prev = round(prev - l.height, 5) + sediment += f" {prev:>10} {l.d50:>10} {l.sigma:>10} {l.critical_constraint:>10}" + + f.write(f"{x}{y}{z} {n} {sediment}\n") f.write(f" 999.9990 999.9990 999.9990\n") @@ -182,6 +206,7 @@ class Mage(AbstractSolver): AVA = [] HYD = [] LIM = [] + QSO = [] for tab in ["liquid", "solid", "suspenssion"]: for bound in lst.get_tab(tab): @@ -191,10 +216,13 @@ class Mage(AbstractSolver): HYD.append(bound) elif bound.bctype == "TZ": LIM.append(bound) + elif bound.bctype == "SL": + QSO.append(bound) files = files + self._export_BC("AVA", AVA, repertory, qlog) files = files + self._export_BC("HYD", HYD, repertory, qlog) files = files + self._export_BC("LIM", LIM, repertory, qlog) + files = files + self._export_QSO(QSO, repertory, qlog) return files @@ -332,12 +360,20 @@ class Mage(AbstractSolver): for file in files: EXT = file.split('.')[1] - f.write(f"{EXT} {file}\n") + if EXT not in ["ST", "GRA"]: + f.write(f"{EXT} {file}\n") f.write("* OUTPUT\n") f.write(f"TRA 0.TRA\n") f.write(f"BIN 0.BIN\n") + for file in files: + EXT = file.split('.')[1] + + if EXT in ["GRA"]: + f.write(f"{EXT} {file}\n") + + @timer def export(self, study, repertory, qlog = None): self._export_ST(study, repertory, qlog) @@ -459,11 +495,37 @@ class Mage8(Mage): return files + @timer + def _export_QSO(self, bounds, repertory, qlog): + files = [] + + if len(bounds) == 0: + return files + + if qlog is not None: + qlog.put(f"Export QSO file") + + with mage_file_open(os.path.join(repertory, f"0.QSO"), "w+") as f: + files.append(f"0.QSO") + + for bound in bounds: + name = f"{bound.node.id:3}".replace(" ", "x") + f.write(f"* {bound.node.name} ({name}) {bound.bctype}\n") + f.write(f"${name}\n") + header = bound.header + f.write(f"*{header[0]:>9}|{header[1]:>10}\n") + + for d in bound.data: + f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n") + + return files + + @timer def export(self, study, repertory, qlog = None): files = [] - self._export_ST(study, repertory, qlog) + files = self._export_ST(study, repertory, qlog) files = files + self._export_PAR(study, repertory, qlog) files = files + self._export_NET(study, repertory, qlog) files = files + self._export_bound_cond(study, repertory, qlog) diff --git a/src/View/BoundaryCondition/Edit/translate.py b/src/View/BoundaryCondition/Edit/translate.py index 4c09f5fc..c8039795 100644 --- a/src/View/BoundaryCondition/Edit/translate.py +++ b/src/View/BoundaryCondition/Edit/translate.py @@ -26,5 +26,6 @@ table_headers = { "time": _translate("BoundaryCondition", "Time"), "date": _translate("BoundaryCondition", "Date"), "discharge": _translate("BoundaryCondition", "Discharge (m³/s)"), - "z": _translate("BoundaryCondition", "Z (m)") + "z": _translate("BoundaryCondition", "Z (m)"), + "solid": _translate("BoundaryCondition", "Solid (kg/s)"), } diff --git a/src/View/BoundaryCondition/translate.py b/src/View/BoundaryCondition/translate.py index 661c1ad0..c4ff8f88 100644 --- a/src/View/BoundaryCondition/translate.py +++ b/src/View/BoundaryCondition/translate.py @@ -20,7 +20,8 @@ from PyQt5.QtCore import QCoreApplication from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, - TimeOverZ, TimeOverDischarge, ZOverDischarge + TimeOverZ, TimeOverDischarge, ZOverDischarge, + Solid ) _translate = QCoreApplication.translate @@ -31,6 +32,7 @@ long_types = { "TZ": _translate("BoundaryCondition", "Time over Z"), "TD": _translate("BoundaryCondition", "Time over Discharge"), "ZD": _translate("BoundaryCondition", "Z over Discharge"), + "SL": _translate("BoundaryCondition", "Solid"), } table_headers = { @@ -44,5 +46,6 @@ BC_types = { "PC": PonctualContribution, "TZ": TimeOverZ, "TD": TimeOverDischarge, - "ZD": ZOverDischarge + "ZD": ZOverDischarge, + "SL": Solid, } diff --git a/tests_cases/Saar/Saar.pamhyr b/tests_cases/Saar/Saar.pamhyr index df56a2984353c1c2f9d8f73ffc277b4fab764ba6..711ef7bc929574e1ca841b76442e1262bddd4c8b 100644 GIT binary patch literal 192512 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 literal 176128 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 b1?E~{t_9{=V6Fw`T41gP=33zY*B1D1^QhA{ From cd07f3aa8db6cdec7050a4b03ec62c15608e7696 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 21 Aug 2023 10:55:27 +0200 Subject: [PATCH 29/34] Sediment: Add SL to geometry into DB and minor change. --- src/Model/Geometry/PointXYZ.py | 4 +++- src/Model/Geometry/ProfileXYZ.py | 4 +++- src/Model/SolverParameters/SolverParametersList.py | 7 +++++++ src/Model/Study.py | 2 +- src/Solver/Mage.py | 4 ++-- src/View/SolverParameters/translate.py | 4 ++-- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 33a3bebf..19695c77 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -44,7 +44,9 @@ class PointXYZ(Point, SQLSubModel): y INTEGER NOT NULL, z INTEGER NOT NULL, profile INTEGER NOT NULL, - FOREIGN KEY(profile) REFERENCES profileXYZ(id) + sl INTEGER, + FOREIGN KEY(profile) REFERENCES profileXYZ(id), + FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) ) """) diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 12c4ea77..e818eabc 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -76,7 +76,9 @@ class ProfileXYZ(Profile, SQLSubModel): num INTEGER NOT NULL, code1 INTEGER NOT NULL, code2 INTEGER NOT NULL, - FOREIGN KEY(reach) REFERENCES river_reach(id) + sl INTEGER, + FOREIGN KEY(reach) REFERENCES river_reach(id), + FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) ) """) diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index a6471356..ce62ff7c 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -93,6 +93,13 @@ class SolverParametersList(SQLSubModel): @classmethod def _sql_update(cls, execute, version): + major, minor, release = version.strip().split(".") + + if major == minor == "0": + if int(release) < 3: + execute(f"UPDATE solver_parameter SET name='mage_implicitation' WHERE name='mage_implication'") + execute(f"UPDATE solver_parameter SET name='mage_iteration_type' WHERE name='mage_iter_type'") + return cls._update_submodel(execute, version) @classmethod diff --git a/src/Model/Study.py b/src/Model/Study.py index c907da3e..5b44d0a7 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -40,7 +40,7 @@ class Study(SQLModel): def __init__(self, filename = None, init_new = True): # Metadata - self._version = "0.0.2" + self._version = "0.0.3" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 81f99722..77cc83d3 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -61,11 +61,11 @@ class Mage(AbstractSolver): ("mage_timestep_tra", "3600"), ("mage_timestep_bin", "0"), # ("mage_timestep_melissa", "0"), - ("mage_implication", "0.70"), + ("mage_implicitation", "0.70"), ("mage_continuity_discretization", "S"), ("mage_qsj_discretization", "B"), ("mage_stop_criterion_iterations", "R"), - ("mage_iter_type", "0"), + ("mage_iteration_type", "0"), ("mage_smooth_coef", "0"), ("mage_cfl_max", "-1."), ("mage_min_height", "0.1"), diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 76cb70ac..3d07e6b3 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -59,11 +59,11 @@ def init(): "mage_min_timestep": _translate("SolverParameters", "Minimum timestep (second)"), "mage_timestep_tra": _translate("SolverParameters", "Time step of writing on .TRA"), "mage_timestep_bin": _translate("SolverParameters", "Time step of writing on .BIN"), - "mage_implication": _translate("SolverParameters", "Implicitation parameter"), + "mage_implicitation": _translate("SolverParameters", "Implicitation parameter"), "mage_continuity_discretization": _translate("SolverParameters", "Continuity discretization type (S/L)"), "mage_qsj_discretization": _translate("SolverParameters", "QSJ discretization (A/B)"), "mage_stop_criterion_iterations": _translate("SolverParameters", "Stop criterion iterations (G/A/R)"), - "mage_iter_type": _translate("SolverParameters", "Iteration type"), + "mage_iteration_type": _translate("SolverParameters", "Iteration type"), "mage_smooth_coef": _translate("SolverParameters", "Smoothing coefficient"), "mage_cfl_max": _translate("SolverParameters", "Maximun accepted number of CFL"), "mage_min_height": _translate("SolverParameters", "Minimum water height (meter)"), From d58a509c1d3def03f17758145da52355e3a79154 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 21 Aug 2023 11:37:01 +0200 Subject: [PATCH 30/34] Solver: Add custom command line parameters and solver default command line args. --- src/Model/River.py | 15 ++++++ .../SolverParameters/SolverParametersList.py | 11 ++++ src/Solver/ASolver.py | 51 ++++++++++++------- src/Solver/Mage.py | 16 ++++++ src/View/RunSolver/Window.py | 3 +- src/View/SolverParameters/translate.py | 1 + 6 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/Model/River.py b/src/Model/River.py index 3041c6c7..91d765ba 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +from tools import flatten + from Model.DB import SQLSubModel from Model.Network.Node import Node @@ -382,3 +384,16 @@ class River(Graph, SQLSubModel): def set_current_reach(self, reach): self._current_reach = reach + + def has_sediment(self): + has = len(self._sediment_layers) != 0 + has &= any( + filter( + lambda p: p.sl != None, + flatten( + map(lambda e: e.reach.profiles, self.edges()) + ) + ) + ) + + return has diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index ce62ff7c..18c45529 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -167,6 +167,17 @@ class SolverParametersList(SQLSubModel): def get(self, index): return self._parameters[index] + def get_by_key(self, key): + try: + return next( + filter( + lambda p: p["name"] == key, + self._parameters + ) + )["value"] + except: + return None + def set(self, index, new): self._parameters[index] = new self._status.modified() diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index ea268999..186abbbb 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -86,6 +86,7 @@ class AbstractSolver(object): ("all_init_time", "000:00:00:00"), ("all_final_time", "999:99:00:00"), ("all_timestep", "300.0"), + ("all_command_line_arguments", ""), ] return lst @@ -153,6 +154,17 @@ class AbstractSolver(object): def export(self, study, repertory, qlog = None): raise NotImplementedMethodeError(self, self.export) + def cmd_args(self, study): + """Return solver command line arguments list + + Returns: + Command line arguments list + """ + params = study.river.get_params(self.type) + args = params.get_by_key("all_command_line_arguments") + + return args.split(" ") + def input_param(self): """Return input command line parameter(s) @@ -190,7 +202,7 @@ class AbstractSolver(object): ) ) - def _format_command(self, cmd, path = ""): + def _format_command(self, study, cmd, path = ""): """Format command line Args: @@ -206,6 +218,7 @@ class AbstractSolver(object): cmd = cmd.replace("@path", path.replace(" ", "\ ")) cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) + cmd = cmd.replace("@args", " ".join(self.cmd_args(study))) logger.debug(f"! {cmd}") @@ -233,13 +246,13 @@ class AbstractSolver(object): logger.info(f"! {exe} {args}") return exe, args - def run_input_data_fomater(self): + def run_input_data_fomater(self, study): if self._cmd_input == "": - self._run_next() + self._run_next(study) return True cmd = self._cmd_input - exe, args = self._format_command(cmd, self._path_input) + exe, args = self._format_command(study, cmd, self._path_input) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -252,13 +265,13 @@ class AbstractSolver(object): return True - def run_solver(self): + def run_solver(self, study): if self._cmd_solver == "": - self._run_next() + self._run_next(study) return True cmd = self._cmd_solver - exe, args = self._format_command(cmd, self._path_solver) + exe, args = self._format_command(study, cmd, self._path_solver) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -272,13 +285,13 @@ class AbstractSolver(object): self._status = STATUS.RUNNING return True - def run_output_data_fomater(self): + def run_output_data_fomater(self, study): if self._cmd_output == "": - self._run_next() + self._run_next(study) return True cmd = self._cmd_output - exe, args = self._format_command(cmd, self._path_output) + exe, args = self._format_command(study, cmd, self._path_output) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -297,29 +310,29 @@ class AbstractSolver(object): for x in s.split('\n'): self._output.put(x) - def _run_next(self): + def _run_next(self, study): self._step += 1 if self._step < len(self._runs): - res = self._runs[self._step]() + res = self._runs[self._step](study) if res is not True: self._output.put(res) else: self._status = STATUS.STOPED - def _finished(self, exit_code, exit_status): + def _finished(self, study, exit_code, exit_status): if self._output is not None: self._output.put(exit_code) - self._run_next() + self._run_next(study) - def run(self, process = None, output_queue = None): + def run(self, study, process = None, output_queue = None): if process is not None: self._process = process if output_queue is not None: self._output = output_queue self._process.readyRead.connect(self._data_ready) - self._process.finished.connect(self._finished) + self._process.finished.connect(lambda c, s: self._finished(study, c, s)) self._runs = [ self.run_input_data_fomater, @@ -328,7 +341,7 @@ class AbstractSolver(object): ] self._step = 0 # Run first step - res = self._runs[0]() + res = self._runs[0](study) if res is not True: self._output.put(res) @@ -340,14 +353,14 @@ class AbstractSolver(object): self._status = STATUS.STOPED return True - def start(self, process = None): + def start(self, study, process = None): if _signal: if self._status == STATUS.PAUSED: os.kill(self._process.pid(), SIGCONT) self._status = STATUS.RUNNING return True - self.run(process) + self.run(study, process) return True def pause(self): diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 77cc83d3..5e7909d7 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -97,6 +97,14 @@ class Mage(AbstractSolver): # Export # ########## + def cmd_args(self, study): + l = super(Mage, self).cmd_args(study) + + l.append("-r") + l.append("-fp=1") + + return l + def input_param(self): return "0.REP" @@ -440,6 +448,14 @@ class Mage8(Mage): # Export # ########## + def cmd_args(self, study): + l = super(Mage8, self).cmd_args(study) + + if study.river.has_sediment(): + l.append("-c=3") + + return l + @timer def _export_PAR(self, study, repertory, qlog = None): files = [] diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 872d72ef..c307157a 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -147,6 +147,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self._log(f" *** Run solver {self._solver.name}", color="blue") self._solver.run( + study, process = self._process, output_queue = self._output ) @@ -220,7 +221,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self._log(" *** Start", color="blue") self._results = None - self._solver.start(self._process) + self._solver.start(self._study, process = self._process) self.find(QAction, "action_start").setEnabled(False) if _signal: diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 3d07e6b3..2508629e 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -55,6 +55,7 @@ def init(): "all_init_time": _translate("SolverParameters", "Initial time (jj:hh:mm:ss)"), "all_final_time": _translate("SolverParameters", "Final time (jj:hh:mm:ss)"), "all_timestep": _translate("SolverParameters", "Timestep (second)"), + "all_command_line_arguments": _translate("SolverParameters", "Command line arguments"), # Mage specific parameters "mage_min_timestep": _translate("SolverParameters", "Minimum timestep (second)"), "mage_timestep_tra": _translate("SolverParameters", "Time step of writing on .TRA"), From 596cd24cc33977197181b11017b3674896b35603 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 21 Aug 2023 16:18:28 +0200 Subject: [PATCH 31/34] Sediment: Add GRA file reading. --- src/Model/Results/River/River.py | 3 + src/Solver/Mage.py | 162 ++++++++++++++++++++++++++++++- 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 5ae1e90f..9f72f731 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -98,6 +98,9 @@ class River(object): def reach(self, id): return self._reachs[id] + def has_reach(self, id): + return 0 <= id < len(self._reachs) + def add(self, reach_id): reachs = self._study.river.enable_edges() diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 5e7909d7..9bc9331e 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -101,7 +101,6 @@ class Mage(AbstractSolver): l = super(Mage, self).cmd_args(study) l.append("-r") - l.append("-fp=1") return l @@ -475,7 +474,6 @@ class Mage8(Mage): f.write(f"{name} {value}\n") - return files @timer @@ -555,6 +553,7 @@ class Mage8(Mage): # RESULTS # ########### + @timer def read_bin(self, study, repertory, results, qlog = None): logger.info(f"read_bin: Start ...") @@ -675,3 +674,162 @@ class Mage8(Mage): logger.debug(reachs[0].profiles[0]._data) results.set("timestamps", ts) logger.info(f"read_bin: ... end with {len(ts)} timestamp read") + + @timer + def read_gra(self, study, repertory, results, qlog = None): + # if not study.river.has_sediment(): + # return + + logger.info(f"read_gra: Start ...") + + with mage_file_open(os.path.join(repertory, f"0.GRA"), "r") as f: + newline = lambda: np.fromfile(f, dtype=np.int32, count=1) + endline = lambda: np.fromfile(f, dtype=np.int32, count=1) + + read_int = lambda size: np.fromfile(f, dtype=np.int32, count=size) + read_float = lambda size: np.fromfile(f, dtype=np.float32, count=size) + read_float64 = lambda size: np.fromfile(f, dtype=np.float64, count=size) + + # Meta data (1st line) + newline() + + data = read_int(3) + + nb_reach = data[0] + nb_profile = data[1] + mage_version = data[2] + + logger.debug(f"read_gra: nb_reach = {nb_reach}") + logger.debug(f"read_gra: nb_profile = {nb_profile}") + logger.debug(f"read_gra: mage_version = {mage_version}") + + if mage_version <= 80: + msg = ( + "Read GRA files: " + + f"Possible incompatible mage version '{mage_version}', " + + "please check your solver configuration..." + ) + logger.warning(msg) + + if qlog is not None: + qlog.put("[WARNING] " + msg) + + results.set("gra_solver_version", f"Mage8 ({mage_version})") + results.set("gra_nb_reach", f"{nb_reach}") + results.set("gra_nb_profile", f"{nb_profile}") + + endline() + + # Reach information (2nd line) + newline() + + reachs = [] + iprofiles = {} + reach_offset = {} + + data = read_int(2*nb_reach) + + for i in range(nb_reach): + # Get results reach to reach list + r = results.river.reach(i) + reachs.append(r) + + # ID of first and last reach profiles + i1 = data[2*i] - 1 + i2 = data[2*i+1] - 1 + + # Add profile id correspondance to reach + key = (i1, i2) + iprofiles[key] = r + + # Profile ID offset + reach_offset[r] = i1 + + logger.debug(f"read_gra: iprofiles = {iprofiles}") + + endline() + + # X (3rd line) + newline() + _ = read_float(nb_profile) + endline() + + # npts (4th line) + newline() + _ = read_int(nb_profile) + endline() + + # Data + ip_to_r = lambda i: iprofiles[ + next( + filter( + lambda k: k[0] <= i <= k[1], + iprofiles + ) + ) + ] + ip_to_ri = lambda r, i: i - reach_offset[r] + + ts = set() + ind = 0 + end = False + + newline() + while not end: + n = read_int(1)[0] + timestamp = read_float64(1)[0] + + logger.debug(f"read_gra: timestamp = {timestamp} sec") + ts.add(timestamp) + + endline() + + for i in range(n): + newline() + nsl = read_int(1)[0] + endline() + + # Get current profile id + reach = ip_to_r(i) + ri = ip_to_ri(reach, i) + + if nsl > 1: + logger.warning( + "read_gra: " + + "Multiple sediment layers for one profile " + + "is not implemented yet..." + ) + + for j in range(nsl): + newline() + nl = read_int(1)[0] + endline() + + sl = [] + + for k in range(nl): + newline() + data = read_float64(3) + endline() + + h = data[0] + d50 = data[1] + sigma = data[2] + + sl.append((h, d50, sigma)) + + reach.set(ri, timestamp, "sl", sl) + + ind += 1 + end = newline().size <= 0 + + logger.debug(reachs[0].profiles[0]._data) + results.set("timestamps", ts) + logger.info(f"read_gra: ... end with {len(ts)} timestamp read") + + @timer + def results(self, study, repertory, qlog = None): + results = super(Mage8, self).results(study, repertory, qlog) + self.read_gra(study, repertory, results, qlog) + + return results From d7a8e5de20758ac1b69cfba58fd98d22bfa194e0 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 21 Aug 2023 17:21:10 +0200 Subject: [PATCH 32/34] Results, Sediment: Add sediment results plots. --- src/View/Results/PlotSedProfile.py | 130 ++++++++++++++++++++++++++++ src/View/Results/PlotSedReach.py | 134 +++++++++++++++++++++++++++++ src/View/Results/Window.py | 41 +++++++++ src/View/ui/Results.ui | 28 +++++- 4 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/View/Results/PlotSedProfile.py create mode 100644 src/View/Results/PlotSedReach.py diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py new file mode 100644 index 00000000..5b34bb1e --- /dev/null +++ b/src/View/Results/PlotSedProfile.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class PlotSedProfile(APlot): + def __init__(self, canvas=None, study=None, results=None, + reach_id=0, profile_id=0, + toolbar=None): + super(PlotSedProfile, self).__init__( + canvas=canvas, + data=study, + toolbar=toolbar + ) + + self._results = results + + self._current_timestamp = max(results.get("timestamps")) + self._current_reach_id = reach_id + self._current_profile_id = profile_id + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + reach = self._results.river.reach(self._current_reach_id) + profile = reach.profile(self._current_profile_id) + if profile.geometry.number_points == 0: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + x = profile.geometry.get_station() + z = profile.geometry.z() + + psl = list( + map( + lambda sl: sl[0], + profile.get_ts_key(self._current_timestamp, "sl") + ) + ) + + sl = [] + for i in range(len(psl)): + cur = [] + for p in range(profile.geometry.number_points): + cur.append(psl[i]) + sl.append(cur) + + # logger.info(sl) + + self.canvas.axes.set_xlim( + left = min(x), right = max(x) + ) + + # Compute sediment layer in function to point z + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl, + [z] + ) + + self.line_kp_sl = [] + for i, zsl in enumerate(reversed(z_sl)): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + x, zsl, + linestyle="solid" if i == len(z_sl) - 1 else "--", + lw=1.8, + color='grey' if i == len(z_sl) - 1 else None + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = False + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + if ind is None: + logger.info("TODO: Update") + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.canvas.draw_idle() + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + self.draw() + + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.draw() diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py new file mode 100644 index 00000000..892120c6 --- /dev/null +++ b/src/View/Results/PlotSedReach.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class PlotSedReach(APlot): + def __init__(self, canvas=None, study=None, results=None, + reach_id=0, profile_id=0, + toolbar=None): + super(PlotSedReach, self).__init__( + canvas=canvas, + data=study, + toolbar=toolbar + ) + + self._results = results + + self._current_timestamp = max(results.get("timestamps")) + self._current_reach_id = reach_id + self._current_profile_id = profile_id + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + reach = self._results.river.reach(self._current_reach_id) + if reach.geometry.number_profiles == 0: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "Kp (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + kp = reach.geometry.get_kp() + z_min = reach.geometry.get_z_min() + z_max = reach.geometry.get_z_max() + + psl = list( + map( + # Get SL list for profile p + lambda p: p.get_ts_key(self._current_timestamp, "sl"), + reach.profiles + ) + ) + + max_sl = reduce( + lambda acc, sl: max(acc, len(sl)), + psl, + 0 + ) + + sl = [] + for i in range(max_sl): + cur = [] + for csl in psl: + if i < len(csl): + cur.append(csl[i][0]) + else: + cur.append(0) + sl.append(cur) + + # logger.info(f"sl = {sl}") + + self.canvas.axes.set_xlim( + left = min(kp), right = max(kp) + ) + + # Compute sediment layer in function to profile z_min + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl, + [z_min] + ) + + self.line_kp_sl = [] + for i, z in enumerate(reversed(z_sl)): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + kp, z, + linestyle="solid" if i == len(z_sl) - 1 else "--", + lw=1.8, + color='grey' if i == len(z_sl) - 1 else None + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = False + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + self.draw() + + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.draw() diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 22d0a2c4..6ca6fc56 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -46,6 +46,8 @@ from View.Results.PlotXY import PlotXY from View.Results.PlotAC import PlotAC from View.Results.PlotKPC import PlotKPC from View.Results.PlotH import PlotH +from View.Results.PlotSedReach import PlotSedReach +from View.Results.PlotSedProfile import PlotSedProfile from View.Results.Table import TableModel from View.Results.translate import * @@ -180,6 +182,37 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): ) self.plot_h.draw() + + self.canvas_5 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_5.setObjectName("canvas_5") + self.plot_layout_5 = self.find(QVBoxLayout, "verticalLayout_sed_reach") + self.plot_layout_5.addWidget(self.canvas_5) + + self.plot_sed_reach = PlotSedReach( + canvas = self.canvas_5, + results = self._results, + study = self._study, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_sed_reach.draw() + + self.canvas_6 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_6.setObjectName("canvas_6") + self.plot_layout_6 = self.find(QVBoxLayout, "verticalLayout_sed_profile") + self.plot_layout_6.addWidget(self.canvas_6) + + self.plot_sed_profile = PlotSedProfile( + canvas = self.canvas_6, + results = self._results, + study = self._study, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_sed_profile.draw() + def setup_connections(self): self.undo_sc.activated.connect(self.undo) self.redo_sc.activated.connect(self.redo) @@ -210,21 +243,29 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_ac.set_reach(reach_id) self.plot_kpc.set_reach(reach_id) self.plot_h.set_reach(reach_id) + self.plot_sed_reach.set_reach(reach_id) + self.plot_sed_profile.set_reach(reach_id) if profile_id is not None: self.plot_xy.set_profile(profile_id) self.plot_ac.set_profile(profile_id) self.plot_kpc.set_profile(profile_id) self.plot_h.set_profile(profile_id) + self.plot_sed_reach.set_profile(profile_id) + self.plot_sed_profile.set_profile(profile_id) if timestamp is not None: self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) self.plot_kpc.set_timestamp(timestamp) self.plot_h.set_timestamp(timestamp) + self.plot_sed_reach.set_timestamp(timestamp) + self.plot_sed_profile.set_timestamp(timestamp) self.plot_xy.draw() self.plot_ac.draw() self.plot_kpc.draw() self.plot_h.draw() + self.plot_sed_reach.draw() + self.plot_sed_profile.draw() def _set_current_reach(self): diff --git a/src/View/ui/Results.ui b/src/View/ui/Results.ui index d975641b..5b797724 100644 --- a/src/View/ui/Results.ui +++ b/src/View/ui/Results.ui @@ -30,7 +30,7 @@ - + @@ -75,6 +75,32 @@ + + + true + + + Sediment + + + + + + true + + + Qt::Vertical + + + + + + + + + + + From bab1ce9e177998a33a5b21581e2f74496764849c Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 22 Aug 2023 11:03:50 +0200 Subject: [PATCH 33/34] Results: Display custom time in hydrograph. --- src/Solver/Mage.py | 2 +- src/View/Results/PlotH.py | 52 +++++++++++++++++++++++-- src/View/ui/Widgets/extendedTimeEdit.ui | 3 ++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 9bc9331e..a9273176 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -201,7 +201,7 @@ class Mage(AbstractSolver): f.write(f"*{header[0]:>9}|{header[1]:>10}\n") for d in bound.data: - f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n") + f.write(f"{d[0]:10}{d[1]:10}\n") return files diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py index dcc9f7f6..47c3f1bf 100644 --- a/src/View/Results/PlotH.py +++ b/src/View/Results/PlotH.py @@ -19,6 +19,7 @@ import logging from functools import reduce +from datetime import datetime from tools import timer, trace from View.Plot.APlot import APlot @@ -34,14 +35,14 @@ logger = logging.getLogger() class PlotH(APlot): def __init__(self, canvas=None, results=None, reach_id=0, profile_id=0, - toolbar=None, display_current=True): + toolbar=None): super(PlotH, self).__init__( canvas=canvas, data=results, toolbar=toolbar ) - self.display_current = display_current + self._mode = "time" self._current_timestamp = max(results.get("timestamps")) self._current_reach_id = reach_id @@ -99,6 +100,10 @@ class PlotH(APlot): ) return + self.canvas.axes.set_ylim( + [min(min(y),0), max(y)] + ) + self._line = [ self.canvas.axes.plot( x, y, lw=1., @@ -107,8 +112,47 @@ class PlotH(APlot): ) ] - self.canvas.axes.autoscale_view(True, True, True) - self.canvas.axes.autoscale() + # Custom time display + nb = len(x) + mod = int(nb / 5) + mod = mod if mod > 0 else nb + + fx = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] % mod == 0, + enumerate(x) + ) + ) + ) + + if self._mode == "time": + t0 = datetime.fromtimestamp(0) + xt = list( + map( + lambda v: ( + str( + datetime.fromtimestamp(v) - t0 + ).split(",")[0]\ + .replace("days", _translate("Results", "days"))\ + .replace("day", _translate("Results", "day")) + ), + fx + ) + ) + else: + xt = list( + map( + lambda v: str(datetime.fromtimestamp(v).date()), + fx + ) + ) + + self.canvas.axes.set_xticks(ticks=fx, labels=xt, rotation=45) + + # self.canvas.axes.autoscale_view(True, True, True) + # self.canvas.axes.autoscale() self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() if self.toolbar is not None: diff --git a/src/View/ui/Widgets/extendedTimeEdit.ui b/src/View/ui/Widgets/extendedTimeEdit.ui index 493a96a1..eddf3892 100644 --- a/src/View/ui/Widgets/extendedTimeEdit.ui +++ b/src/View/ui/Widgets/extendedTimeEdit.ui @@ -28,6 +28,9 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 999999 + From c1ff99797ceac24ddbf5a70296b3c5b6578c4f24 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 22 Aug 2023 11:39:08 +0200 Subject: [PATCH 34/34] Resutls: Read and display sediment results only if available. --- src/Solver/Mage.py | 4 +-- src/View/Results/Window.py | 63 ++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index a9273176..aa68802f 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -677,8 +677,8 @@ class Mage8(Mage): @timer def read_gra(self, study, repertory, results, qlog = None): - # if not study.river.has_sediment(): - # return + if not study.river.has_sediment(): + return logger.info(f"read_gra: Start ...") diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 6ca6fc56..3882a7ba 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -182,36 +182,37 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): ) self.plot_h.draw() - self.canvas_5 = MplCanvas(width=5, height=4, dpi=100) self.canvas_5.setObjectName("canvas_5") self.plot_layout_5 = self.find(QVBoxLayout, "verticalLayout_sed_reach") self.plot_layout_5.addWidget(self.canvas_5) - self.plot_sed_reach = PlotSedReach( - canvas = self.canvas_5, - results = self._results, - study = self._study, - reach_id = 0, - profile_id = 0, - toolbar = None - ) - self.plot_sed_reach.draw() + if self._study.river.has_sediment(): + self.plot_sed_reach = PlotSedReach( + canvas = self.canvas_5, + results = self._results, + study = self._study, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_sed_reach.draw() self.canvas_6 = MplCanvas(width=5, height=4, dpi=100) self.canvas_6.setObjectName("canvas_6") self.plot_layout_6 = self.find(QVBoxLayout, "verticalLayout_sed_profile") self.plot_layout_6.addWidget(self.canvas_6) - self.plot_sed_profile = PlotSedProfile( - canvas = self.canvas_6, - results = self._results, - study = self._study, - reach_id = 0, - profile_id = 0, - toolbar = None - ) - self.plot_sed_profile.draw() + if self._study.river.has_sediment(): + self.plot_sed_profile = PlotSedProfile( + canvas = self.canvas_6, + results = self._results, + study = self._study, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_sed_profile.draw() def setup_connections(self): self.undo_sc.activated.connect(self.undo) @@ -243,29 +244,37 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_ac.set_reach(reach_id) self.plot_kpc.set_reach(reach_id) self.plot_h.set_reach(reach_id) - self.plot_sed_reach.set_reach(reach_id) - self.plot_sed_profile.set_reach(reach_id) + + if self._study.river.has_sediment(): + self.plot_sed_reach.set_reach(reach_id) + self.plot_sed_profile.set_reach(reach_id) if profile_id is not None: self.plot_xy.set_profile(profile_id) self.plot_ac.set_profile(profile_id) self.plot_kpc.set_profile(profile_id) self.plot_h.set_profile(profile_id) - self.plot_sed_reach.set_profile(profile_id) - self.plot_sed_profile.set_profile(profile_id) + + if self._study.river.has_sediment(): + self.plot_sed_reach.set_profile(profile_id) + self.plot_sed_profile.set_profile(profile_id) if timestamp is not None: self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) self.plot_kpc.set_timestamp(timestamp) self.plot_h.set_timestamp(timestamp) - self.plot_sed_reach.set_timestamp(timestamp) - self.plot_sed_profile.set_timestamp(timestamp) + + if self._study.river.has_sediment(): + self.plot_sed_reach.set_timestamp(timestamp) + self.plot_sed_profile.set_timestamp(timestamp) self.plot_xy.draw() self.plot_ac.draw() self.plot_kpc.draw() self.plot_h.draw() - self.plot_sed_reach.draw() - self.plot_sed_profile.draw() + + if self._study.river.has_sediment(): + self.plot_sed_reach.draw() + self.plot_sed_profile.draw() def _set_current_reach(self):