diff --git a/src/Model/Reservoir/Reservoir.py b/src/Model/Reservoir/Reservoir.py
new file mode 100644
index 00000000..97a63113
--- /dev/null
+++ b/src/Model/Reservoir/Reservoir.py
@@ -0,0 +1,268 @@
+# Reservoir.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from Model.Tools.PamhyrDB import SQLSubModel
+
+logger = logging.getLogger()
+
+
+class Reservoir(SQLSubModel):
+ _sub_classes = []
+ _id_cnt = 0
+
+ def __init__(self, id: int = -1, name: str = "",
+ status=None):
+ super(Reservoir, self).__init__()
+
+ self._status = status
+
+ if id == -1:
+ self.id = Reservoir._id_cnt
+ else:
+ self.id = id
+
+ self._name = name
+ self._node = None
+ self._data = []
+
+ Reservoir._id_cnt = max(Reservoir._id_cnt + 1, self.id)
+
+ @classmethod
+ def _db_create(cls, execute):
+ execute("""
+ CREATE TABLE reservoir(
+ id INTEGER NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL,
+ node INTEGER,
+ FOREIGN KEY(node) REFERENCES river_node(id)
+ )
+ """)
+
+ execute("""
+ CREATE TABLE reservoir_data(
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ ind INTEGER NOT NULL,
+ elevation REAL NOT NULL,
+ surface REAL NOT NULL,
+ reservoir INTEGER,
+ FOREIGN KEY(reservoir) REFERENCES reservoir(id)
+ )
+ """)
+
+ return cls._create_submodel(execute)
+
+ @classmethod
+ def _db_update(cls, execute, version):
+ major, minor, release = version.strip().split(".")
+ if major == minor == "0":
+ if int(release) < 5:
+ cls._db_create(execute)
+
+ return True
+
+ @classmethod
+ def _db_load(cls, execute, data=None):
+ new = []
+
+ table = execute(
+ "SELECT id, name, node " +
+ "FROM reservoir "
+ )
+
+ for row in table:
+ id = row[0]
+ name = row[1]
+ node_id = row[2]
+ new_reservoir = cls(id, name, status=data["status"])
+
+ new_reservoir._node = None
+ if node_id != -1:
+ new_reservoir._node = next(filter(lambda n: n.id == node_id, data["nodes"]))
+
+ new_data = []
+ table = execute(
+ "SELECT elevation, surface " +
+ "FROM reservoir_data " +
+ f"WHERE reservoir = {id} " +
+ "ORDER BY ind ASC"
+ )
+ for t in table:
+ new_data.append((t[0], t[1]))
+
+ new_reservoir._data = new_data
+
+ new.append(new_reservoir)
+
+ return new
+
+ def _db_save(self, execute, data=None):
+
+ execute(f"DELETE FROM reservoir WHERE id = {self.id}")
+ execute(f"DELETE FROM reservoir_data WHERE reservoir = {self.id}")
+
+ node_id = -1
+ if self._node is not None:
+ node_id = self._node.id
+
+ sql = (
+ "INSERT INTO " +
+ "reservoir(id, name, node) " +
+ "VALUES (" +
+ f"{self.id}, '{self._db_format(self._name)}', " +
+ f"{node_id}" +
+ ")"
+ )
+ execute(sql)
+
+ ind = 0
+ for d in self._data:
+ sql = (
+ "INSERT INTO " +
+ "reservoir_data(ind, elevation, surface, reservoir) " +
+ f"VALUES ({ind}, '{d[0]}', {d[1]}, {self.id})"
+ )
+ execute(sql)
+ ind += 1
+
+ return True
+
+ def __len__(self):
+ return len(self._data)
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, name):
+ self._name = name
+ self._status.modified()
+
+ @property
+ def node(self):
+ return self._node
+
+ @node.setter
+ def node(self, node):
+ self._node = node
+ self._status.modified()
+
+ def has_node(self):
+ return self._node is not None
+
+ @property
+ def data(self):
+ return self._data.copy()
+
+ @property
+ def _default_elevation(self):
+ return 0.0
+
+ @property
+ def _default_surface(self):
+ return 0.0
+
+ def is_define(self):
+ return len(self._data) != 0
+
+ def new_from_data(self, data):
+
+ try:
+ new_0 = float(data[0])
+ new_1 = float(data[1])
+ except Exception as e:
+ logger.error(e)
+ new_0 = None
+ new_1 = None
+
+ return (new_0, new_1)
+
+ def add(self, index: int):
+ value = (self._default_elevation, self._default_surface)
+ self._data.insert(index, value)
+ self._status.modified()
+ return value
+
+ def insert(self, index: int, value):
+ self._data.insert(index, value)
+ self._status.modified()
+
+ def delete_i(self, indexes):
+ self._data = list(
+ map(
+ lambda e: e[1],
+ filter(
+ lambda e: e[0] not in indexes,
+ enumerate(self.data)
+ )
+ )
+ )
+ self._status.modified()
+
+ def delete(self, els):
+ self._data = list(
+ filter(
+ lambda e: e not in els,
+ self.data
+ )
+ )
+ self._status.modified()
+
+ def sort(self, _reverse=False, key=None):
+ if key is None:
+ self._data.sort(reverse=_reverse)
+ else:
+ self._data.sort(reverse=_reverse, key=key)
+ self._status.modified()
+
+ def get_i(self, index):
+ return self.data[index]
+
+ def get_range(self, _range):
+ lst = []
+ for r in _range:
+ lst.append(r)
+ return lst
+
+ def _set_i_c_v(self, index, column, value):
+ v = list(self._data[index])
+ v[column] = value
+ self._data[index] = tuple(v)
+ self._status.modified()
+
+ def set_i_elevation(self, index: int, value):
+ self._set_i_c_v(index, 0, value)
+
+ def set_i_surface(self, index: int, value):
+ self._set_i_c_v(index, 1, value)
+
+ def move_up(self, index):
+ if index < len(self):
+ next = index - 1
+ d = self._data
+ d[index], d[next] = d[next], d[index]
+ self._status.modified()
+
+ def move_down(self, index):
+ if index >= 0:
+ prev = index + 1
+ d = self._data
+ d[index], d[prev] = d[prev], d[index]
+ self._status.modified()
diff --git a/src/Model/Reservoir/ReservoirList.py b/src/Model/Reservoir/ReservoirList.py
new file mode 100644
index 00000000..60d2b98e
--- /dev/null
+++ b/src/Model/Reservoir/ReservoirList.py
@@ -0,0 +1,87 @@
+# ReservoirList.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+from copy import copy
+from tools import trace, timer
+
+from Model.Tools.PamhyrList import PamhyrModelList
+from Model.Reservoir.Reservoir import Reservoir
+
+
+class ReservoirList(PamhyrModelList):
+ _sub_classes = [
+ Reservoir,
+ ]
+
+ @classmethod
+ def _db_load(cls, execute, data=None):
+ new = cls(status=data['status'])
+
+ new._lst = Reservoir._db_load(
+ execute, data
+ )
+
+ return new
+
+ def _db_save(self, execute, data=None):
+ execute("DELETE FROM reservoir")
+ execute("DELETE FROM reservoir_data")
+
+ if data is None:
+ data = {}
+
+ for reservoir in self._lst:
+ reservoir._db_save(execute, data=data)
+
+ return True
+
+ def new(self, index):
+ r = Reservoir(status=self._status)
+ self._lst.insert(index, r)
+ self._status.modified()
+ return r
+
+ def __copy__(self):
+ new = ReservoirList()
+
+ new._lst = self._lst.copy()
+
+ return new
+
+ def __deepcopy__(self):
+ new = ReservoirList()
+
+ new._lst = self._lst.deepcopy()
+
+ return new
+
+ def copy(self):
+ return copy(self)
+
+ def get_assoc_to_node(self, node):
+ assoc = list(
+ filter(
+ lambda i: i.node is node,
+ self._lst
+ )
+ )
+
+ if len(assoc) > 0:
+ return assoc[0]
+
+ return None
diff --git a/src/Model/River.py b/src/Model/River.py
index 5047b435..84836625 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -36,6 +36,7 @@ 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 Model.Reservoir.ReservoirList import ReservoirList
from Solver.Solvers import solver_type_list
@@ -217,6 +218,7 @@ class River(Graph, SQLSubModel):
StricklersList,
SolverParametersList,
SedimentLayerList,
+ ReservoirList,
]
def __init__(self, status=None):
@@ -234,6 +236,7 @@ class River(Graph, SQLSubModel):
self._stricklers = StricklersList(status=self._status)
self._parameters = {}
self._sediment_layers = SedimentLayerList(status=self._status)
+ self._reservoir = ReservoirList(status=self._status)
@classmethod
def _db_create(cls, execute):
@@ -294,6 +297,12 @@ class River(Graph, SQLSubModel):
data
)
+ # Reservoir
+ new._reservoir = ReservoirList._db_load(
+ execute,
+ data
+ )
+
# Parameters
new._parameters = SolverParametersList._db_load(
execute,
@@ -309,6 +318,7 @@ class River(Graph, SQLSubModel):
objs.append(self._lateral_contribution)
objs.append(self._sediment_layers)
objs.append(self._stricklers)
+ objs.append(self._reservoir)
for solver in self._parameters:
objs.append(self._parameters[solver])
@@ -349,6 +359,10 @@ class River(Graph, SQLSubModel):
return ret[0]
+ @property
+ def reservoir(self):
+ return self._reservoir
+
@property
def parameters(self):
return self._parameters
diff --git a/src/Model/Study.py b/src/Model/Study.py
index e9ccdb41..b163088b 100644
--- a/src/Model/Study.py
+++ b/src/Model/Study.py
@@ -41,7 +41,7 @@ class Study(SQLModel):
def __init__(self, filename=None, init_new=True):
# Metadata
- self._version = "0.0.4"
+ self._version = "0.0.5"
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 4afaf59f..132a701f 100644
--- a/src/Solver/Mage.py
+++ b/src/Solver/Mage.py
@@ -412,6 +412,36 @@ class Mage(CommandLineSolver):
files.append(f"{name}.INI")
return files
+ @timer
+ def _export_CAS(self, study, repertory, qlog, name="0"):
+ files = []
+
+ reservoirs = study.river.reservoir.lst
+ if len(reservoirs) == 0:
+ return files
+
+ if qlog is not None:
+ qlog.put("Export CAS file")
+
+ with mage_file_open(os.path.join(repertory, f"{name}.CAS"), "w+") as f:
+ files.append(f"{name}.CAS")
+
+ for reservoir in reservoirs:
+ reservoir.sort()
+ node = reservoir.node
+ name = f"{node.id:3}".replace(" ", "x")
+ f.write(f"* {node.name} ({name}) Reservoir\n")
+ f.write(f"${name}\n")
+ f.write(f"*{'Elev(m)':>9}|{'Area(ha)':>10}\n")
+
+ for d in reservoir.data:
+ v0 = d[0]
+ v1 = d[1]
+
+ f.write(f"{v0:>10.3f}{v1:>10.3f}\n")
+
+ return files
+
@timer
def _export_REP(self, study, repertory, files, qlog, name="0"):
if qlog is not None:
@@ -653,6 +683,7 @@ class Mage8(Mage):
self._export_bound_cond(study, repertory, qlog, name=name)
files = files + self._export_RUG(study, repertory, qlog, name=name)
files = files + self._export_INI(study, repertory, qlog, name=name)
+ files = files + self._export_CAS(study, repertory, qlog, name=name)
self._export_REP(study, repertory, files, qlog, name=name)
return True
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 57ad9996..04ed5f94 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -47,6 +47,7 @@ from View.About.Window import AboutWindow
from View.Network.Window import NetworkWindow
from View.Geometry.Window import GeometryWindow
from View.BoundaryCondition.Window import BoundaryConditionWindow
+from View.Reservoir.Window import ReservoirWindow
from View.LateralContribution.Window import LateralContributionWindow
from View.InitialConditions.Window import InitialConditionsWindow
from View.Stricklers.Window import StricklersWindow
@@ -101,7 +102,7 @@ define_model_action = [
"action_menu_boundary_conditions", "action_menu_initial_conditions",
"action_menu_edit_friction", "action_menu_edit_lateral_contribution",
"action_menu_run_solver", "action_menu_sediment_layers",
- "action_menu_edit_reach_sediment_layers"
+ "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs"
]
action = (
@@ -191,6 +192,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"action_menu_edit_network": self.open_network,
"action_menu_edit_geometry": self.open_geometry,
"action_menu_boundary_conditions": self.open_boundary_cond,
+ "action_menu_edit_reservoirs": self.open_reservoir,
"action_menu_initial_conditions": self.open_initial_conditions,
"action_menu_edit_friction": self.open_frictions,
"action_menu_edit_lateral_contribution": self.open_lateral_contrib,
@@ -616,6 +618,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
bound = BoundaryConditionWindow(study=self._study, parent=self)
bound.show()
+ def open_reservoir(self):
+ if self.sub_window_exists(
+ ReservoirWindow,
+ data=[self._study, None]
+ ):
+ return
+
+ reservoir = ReservoirWindow(study=self._study, parent=self)
+ reservoir.show()
+
def open_lateral_contrib(self):
if self.sub_window_exists(
LateralContributionWindow,
diff --git a/src/View/Reservoir/Edit/Plot.py b/src/View/Reservoir/Edit/Plot.py
new file mode 100644
index 00000000..8f8f49b8
--- /dev/null
+++ b/src/View/Reservoir/Edit/Plot.py
@@ -0,0 +1,101 @@
+# Plot.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from datetime import datetime
+
+from tools import timer, trace
+from View.Tools.PamhyrPlot import PamhyrPlot
+
+from PyQt5.QtCore import (
+ QCoreApplication
+)
+
+from View.BoundaryCondition.Edit.translate import BCETranslate
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class Plot(PamhyrPlot):
+ def __init__(self, mode="time", data=None,
+ trad=None, canvas=None, toolbar=None,
+ parent=None):
+ super(Plot, self).__init__(
+ canvas=canvas,
+ trad=trad,
+ data=data,
+ toolbar=toolbar,
+ parent=parent
+ )
+
+ self._table_headers = self._trad.get_dict("table_headers")
+ self._mode = mode
+
+ @timer
+ def draw(self):
+ self.canvas.axes.cla()
+ self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5)
+
+ if len(self.data) == 0:
+ self._init = False
+ return
+
+ # Plot data
+ x = list(map(lambda v: v[0], self.data.data))
+ y = list(map(lambda v: v[1], self.data.data))
+ self._line, = self.canvas.axes.plot(
+ x, y,
+ color='r', lw=1.,
+ markersize=5, marker='+',
+ picker=30,
+ )
+
+ # Plot label
+ #header = self.data.header
+ self.canvas.axes.set_xlabel(
+ self._table_headers["z"], color='black', fontsize=10
+ )
+ self.canvas.axes.set_ylabel(
+ self._table_headers["Area"], color='black', fontsize=10
+ )
+
+ self.canvas.axes.autoscale_view(True, True, True)
+ self.canvas.figure.tight_layout()
+ self.canvas.figure.canvas.draw_idle()
+ # self.toolbar.update()
+
+ self._init = True
+
+ @timer
+ def update(self, ind=None):
+ if not self._init:
+ self.draw()
+ return
+
+ x = list(map(lambda v: v[0], self.data.data))
+ y = list(map(lambda v: v[1], self.data.data))
+
+ self._line.set_data(x, y)
+
+ self.canvas.axes.relim()
+ self.canvas.axes.autoscale()
+ self.canvas.figure.tight_layout()
+ self.canvas.figure.canvas.draw_idle()
diff --git a/src/View/Reservoir/Edit/Table.py b/src/View/Reservoir/Edit/Table.py
new file mode 100644
index 00000000..21e0f91b
--- /dev/null
+++ b/src/View/Reservoir/Edit/Table.py
@@ -0,0 +1,141 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+import logging
+import traceback
+
+from datetime import date, time, datetime, timedelta
+
+from tools import trace, timer
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel,
+ QCoreApplication, QModelIndex, pyqtSlot,
+ QRect, QTime, QDateTime,
+)
+
+from PyQt5.QtWidgets import (
+ QTableView, QAbstractItemView, QSpinBox, QItemDelegate,
+)
+
+from View.Reservoir.Edit.UndoCommand import (
+ SetDataCommand, AddCommand, DelCommand,
+ SortCommand, PasteCommand,
+)
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class TableModel(PamhyrTableModel):
+ def data(self, index, role):
+ if role == Qt.TextAlignmentRole:
+ return Qt.AlignHCenter | Qt.AlignVCenter
+
+ if role != Qt.ItemDataRole.DisplayRole:
+ return QVariant()
+
+ row = index.row()
+ column = index.column()
+
+ value = QVariant()
+
+ if 0 <= column < 2:
+ v = self._data.get_i(row)[column]
+ value = f"{v:.4f}"
+
+ return value
+
+ def setData(self, index, value, role=Qt.EditRole):
+ if not index.isValid() or role != Qt.EditRole:
+ return False
+
+ row = index.row()
+ column = index.column()
+
+ try:
+ self._undo.push(
+ SetDataCommand(
+ self._data, row, column, float(value)
+ )
+ )
+ except Exception as e:
+ logger.info(e)
+ logger.debug(traceback.format_exc())
+
+ self.dataChanged.emit(index, index)
+ return True
+
+ def add(self, row, parent=QModelIndex()):
+ self.beginInsertRows(parent, row, row - 1)
+
+ self._undo.push(
+ AddCommand(
+ self._data, row
+ )
+ )
+
+ self.endInsertRows()
+ self.layoutChanged.emit()
+
+ def delete(self, rows, parent=QModelIndex()):
+ self.beginRemoveRows(parent, rows[0], rows[-1])
+
+ self._undo.push(
+ DelCommand(
+ self._data, rows
+ )
+ )
+
+ self.endRemoveRows()
+
+ def sort(self, _reverse, parent=QModelIndex()):
+ self.layoutAboutToBeChanged.emit()
+
+ self._undo.push(
+ SortCommand(
+ self._data, _reverse
+ )
+ )
+
+ self.layoutAboutToBeChanged.emit()
+ self.layoutChanged.emit()
+
+ def paste(self, row, data):
+ if len(data) == 0:
+ return
+
+ self.layoutAboutToBeChanged.emit()
+
+ self._undo.push(
+ PasteCommand(
+ self._data, row,
+ list(
+ map(
+ lambda d: self._data.new_from_data(d),
+ data
+ )
+ )
+ )
+ )
+
+ self.layoutAboutToBeChanged.emit()
+ self.layoutChanged.emit()
diff --git a/src/View/Reservoir/Edit/Translate.py b/src/View/Reservoir/Edit/Translate.py
new file mode 100644
index 00000000..f7660885
--- /dev/null
+++ b/src/View/Reservoir/Edit/Translate.py
@@ -0,0 +1,35 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Tools.PamhyrTranslate import PamhyrTranslate
+
+from View.Reservoir.Translate import ReservoirTranslate
+
+_translate = QCoreApplication.translate
+
+
+class EditReservoirTranslate(ReservoirTranslate):
+ def __init__(self):
+ super(EditReservoirTranslate, self).__init__()
+
+ self._sub_dict["table_headers"] = {
+ "z": _translate("Reservoir", "Elevation (m)"),
+ "Area": _translate("Reservoir", "Area (hectare)"),
+ }
diff --git a/src/View/Reservoir/Edit/UndoCommand.py b/src/View/Reservoir/Edit/UndoCommand.py
new file mode 100644
index 00000000..22b7a459
--- /dev/null
+++ b/src/View/Reservoir/Edit/UndoCommand.py
@@ -0,0 +1,135 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+ QMessageBox, QUndoCommand, QUndoStack,
+)
+
+from Model.Reservoir.Reservoir import Reservoir
+
+logger = logging.getLogger()
+
+
+class SetDataCommand(QUndoCommand):
+ def __init__(self, data, index, column, new_value):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._index = index
+ self._column = column
+ self._old = self._data.get_i(self._index)[self._column]
+ self._new = new_value
+
+ def undo(self):
+ self._data._set_i_c_v(self._index, self._column, self._old)
+
+ def redo(self):
+ self._data._set_i_c_v(self._index, self._column, self._new)
+
+
+class AddCommand(QUndoCommand):
+ def __init__(self, data, index):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._index = index
+ self._new = None
+
+ def undo(self):
+ self._data.delete_i([self._index])
+
+ def redo(self):
+ if self._new is None:
+ self._new = self._data.add(self._index)
+ else:
+ self._data.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+ def __init__(self, data, rows):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._rows = rows
+
+ self._point = []
+ for row in rows:
+ self._point.append((row, self._data.get_i(row)))
+ self._point.sort()
+
+ def undo(self):
+ for row, el in self._point:
+ self._data.insert(row, el)
+
+ def redo(self):
+ self._data.delete_i(self._rows)
+
+
+class SortCommand(QUndoCommand):
+ def __init__(self, data, _reverse):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._reverse = _reverse
+
+ self._old = self._data.data
+ self._indexes = None
+
+ def undo(self):
+ ll = self._data.data
+ self._data.sort(
+ key=lambda x: self._indexes[ll.index(x)]
+ )
+
+ def redo(self):
+ self._data.sort(
+ _reverse=self._reverse,
+ key=lambda x: x[0]
+ )
+ if self._indexes is None:
+ self._indexes = list(
+ map(
+ lambda p: self._old.index(p),
+ self._data.data
+ )
+ )
+ self._old = None
+
+
+class PasteCommand(QUndoCommand):
+ def __init__(self, data, row, hs):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._row = row
+ self._h = hs
+ self._h.reverse()
+
+ def undo(self):
+ self._data.delete_i(
+ range(self._row, self._row + len(self._h))
+ )
+
+ def redo(self):
+ for h in self._h:
+ self._data.insert(self._row, h)
diff --git a/src/View/Reservoir/Edit/Window.py b/src/View/Reservoir/Edit/Window.py
new file mode 100644
index 00000000..34724bea
--- /dev/null
+++ b/src/View/Reservoir/Edit/Window.py
@@ -0,0 +1,217 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import timer, trace
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+from View.Tools.PamhyrWidget import PamhyrWidget
+
+from PyQt5.QtGui import (
+ QKeySequence,
+)
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel, QCoreApplication,
+ pyqtSlot, pyqtSignal,
+)
+
+from PyQt5.QtWidgets import (
+ QDialogButtonBox, QPushButton, QLineEdit,
+ QFileDialog, QTableView, QAbstractItemView,
+ QUndoStack, QShortcut, QAction, QItemDelegate,
+ QHeaderView, QDoubleSpinBox, QVBoxLayout,
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.Reservoir.Edit.Translate import EditReservoirTranslate
+from View.Reservoir.Edit.Table import TableModel
+from View.Reservoir.Edit.Plot import Plot
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class EditReservoirWindow(PamhyrWindow):
+ _pamhyr_ui = "Reservoir"
+ _pamhyr_name = "Edit Reservoir"
+
+ def __init__(self, data=None, study=None, config=None, parent=None):
+ self._data = data
+ trad = EditReservoirTranslate()
+
+ name = self._pamhyr_name
+ if self._data is not None:
+ node_name = (self._data.node.name if self._data.node is not None
+ else _translate("Reservoir", "Not associated"))
+ name = (
+ _translate("Edit Reservoir", self._pamhyr_name) +
+ f" - {study.name} " +
+ f" - {self._data.name} ({self._data.id}) " +
+ f"({node_name})"
+ )
+
+ super(EditReservoirWindow, self).__init__(
+ title=name,
+ study=study,
+ config=config,
+ trad=trad,
+ parent=parent
+ )
+
+ self._hash_data.append(data)
+
+ self.setup_table()
+ self.setup_plot()
+ self.setup_connections()
+
+ def setup_table(self):
+ headers = {}
+ table_headers = self._trad.get_dict("table_headers")
+ #for h in self._data.header:
+ #headers[h] = table_headers[h]
+
+ table = self.find(QTableView, "tableView")
+ self._table = TableModel(
+ table_view=table,
+ table_headers=table_headers,
+ editable_headers=table_headers,
+ #editable_headers=self._data.header,
+ delegates={
+ #"time": self._delegate_time,
+ },
+ data=self._data,
+ undo=self._undo_stack,
+ opt_data=self._study.time_system
+ )
+
+ table.setModel(self._table)
+ table.setSelectionBehavior(QAbstractItemView.SelectRows)
+ table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+ table.setAlternatingRowColors(True)
+
+ def setup_plot(self):
+ self.canvas = MplCanvas(width=5, height=4, dpi=100)
+ self.canvas.setObjectName("canvas")
+ self.toolbar = PamhyrPlotToolbar(
+ self.canvas, self
+ )
+ self.verticalLayout.addWidget(self.toolbar)
+ self.verticalLayout.addWidget(self.canvas)
+
+ self.plot = Plot(
+ canvas=self.canvas,
+ data=self._data,
+ mode=self._study.time_system,
+ trad=self._trad,
+ toolbar=self.toolbar,
+ )
+ self.plot.draw()
+
+ def setup_connections(self):
+ self.find(QAction, "action_add").triggered.connect(self.add)
+ self.find(QAction, "action_delete").triggered.connect(self.delete)
+ self.find(QAction, "action_sort").triggered.connect(self.sort)
+
+ self._table.dataChanged.connect(self.update)
+
+ def update(self):
+ self.plot.update()
+
+ def index_selected_row(self):
+ table = self.find(QTableView, "tableView")
+ return table.selectionModel()\
+ .selectedRows()[0]\
+ .row()
+
+ def index_selected_rows(self):
+ table = self.find(QTableView, "tableView")
+ return list(
+ # Delete duplicate
+ set(
+ map(
+ lambda i: i.row(),
+ table.selectedIndexes()
+ )
+ )
+ )
+
+ def add(self):
+ rows = self.index_selected_rows()
+ if len(self._data) == 0 or len(rows) == 0:
+ self._table.add(0)
+ else:
+ self._table.add(rows[0])
+
+ self.plot.update()
+
+ def delete(self):
+ rows = self.index_selected_rows()
+ if len(rows) == 0:
+ return
+
+ self._table.delete(rows)
+ self.plot.update()
+
+ def sort(self):
+ self._table.sort(False)
+ self.plot.update()
+
+ def _copy(self):
+ rows = self.index_selected_rows()
+
+ table = []
+ #table.append(self._data.header)
+ table.append(self._trad.get_dict("table_headers"))
+
+ data = self._data.data
+ for row in rows:
+ table.append(list(data[row]))
+
+ self.copyTableIntoClipboard(table)
+
+ def _paste(self):
+ header, data = self.parseClipboardTable()
+
+ logger.debug(f"paste: h:{header}, d:{data}")
+
+ if len(data) == 0:
+ return
+
+ row = 0
+ rows = self.index_selected_rows()
+ if len(rows) != 0:
+ row = rows[0]
+
+ self._table.paste(row, data)
+ self.plot.update()
+
+ def _undo(self):
+ self._table.undo()
+ self.plot.update()
+ self.widget_update()
+
+ def _redo(self):
+ self._table.redo()
+ self.plot.update()
+ self.widget_update()
diff --git a/src/View/Reservoir/Table.py b/src/View/Reservoir/Table.py
new file mode 100644
index 00000000..66cdae82
--- /dev/null
+++ b/src/View/Reservoir/Table.py
@@ -0,0 +1,170 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+import logging
+import traceback
+
+from tools import trace, timer
+
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel,
+ QCoreApplication, QModelIndex, pyqtSlot,
+ QRect,
+)
+
+from PyQt5.QtWidgets import (
+ QDialogButtonBox, QPushButton, QLineEdit,
+ QFileDialog, QTableView, QAbstractItemView,
+ QUndoStack, QShortcut, QAction, QItemDelegate,
+ QComboBox,
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from View.Reservoir.UndoCommand import (
+ SetNameCommand, SetNodeCommand,
+ AddCommand, DelCommand, PasteCommand,
+)
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+ def __init__(self, data=None, trad=None, parent=None):
+ super(ComboBoxDelegate, self).__init__(parent)
+
+ self._data = data
+ self._trad = trad
+
+ def createEditor(self, parent, option, index):
+ self.editor = QComboBox(parent)
+
+ self.editor.addItems(
+ [_translate("Reservoir", "Not associated")] +
+ self._data.nodes_names()
+ )
+
+ self.editor.setCurrentText(index.data(Qt.DisplayRole))
+ return self.editor
+
+ def setEditorData(self, editor, index):
+ value = index.data(Qt.DisplayRole)
+ self.editor.currentTextChanged.connect(self.currentItemChanged)
+
+ def setModelData(self, editor, model, index):
+ text = str(editor.currentText())
+ model.setData(index, text)
+ editor.close()
+ editor.deleteLater()
+
+ def updateEditorGeometry(self, editor, option, index):
+ r = QRect(option.rect)
+ if self.editor.windowFlags() & Qt.Popup:
+ if editor.parent() is not None:
+ r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
+ editor.setGeometry(r)
+
+ @pyqtSlot()
+ def currentItemChanged(self):
+ self.commitData.emit(self.sender())
+
+
+class TableModel(PamhyrTableModel):
+ def _setup_lst(self):
+ self._lst = self._data.reservoir
+
+ def rowCount(self, parent):
+ return len(self._lst)
+
+ def data(self, index, role):
+ if role != Qt.ItemDataRole.DisplayRole:
+ return QVariant()
+
+ row = index.row()
+ column = index.column()
+
+ if self._headers[column] == "name":
+ return self._lst.get(row).name
+ elif self._headers[column] == "node":
+ n = self._lst.get(row).node
+ if n is None:
+ return _translate("Reservoir", "Not associated")
+ return n.name
+
+ return QVariant()
+
+ def setData(self, index, value, role=Qt.EditRole):
+ if not index.isValid() or role != Qt.EditRole:
+ return False
+
+ row = index.row()
+ column = index.column()
+
+ try:
+ if self._headers[column] == "name":
+ self._undo.push(
+ SetNameCommand(
+ self._lst, row, value
+ )
+ )
+ elif self._headers[column] == "node":
+ self._undo.push(
+ SetNodeCommand(
+ self._lst, row, self._data.node(value)
+ )
+ )
+ except Exception as e:
+ logger.info(e)
+ logger.debug(traceback.format_exc())
+
+ self.dataChanged.emit(index, index)
+ return True
+
+ def add(self, row, parent=QModelIndex()):
+ self.beginInsertRows(parent, row, row - 1)
+
+ self._undo.push(
+ AddCommand(
+ self._lst, row
+ )
+ )
+
+ self.endInsertRows()
+ self.layoutChanged.emit()
+
+ def delete(self, rows, parent=QModelIndex()):
+ self.beginRemoveRows(parent, rows[0], rows[-1])
+
+ self._undo.push(
+ DelCommand(
+ self._lst, rows
+ )
+ )
+
+ self.endRemoveRows()
+ 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/Reservoir/Translate.py b/src/View/Reservoir/Translate.py
new file mode 100644
index 00000000..556c9381
--- /dev/null
+++ b/src/View/Reservoir/Translate.py
@@ -0,0 +1,33 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Tools.PamhyrTranslate import PamhyrTranslate
+
+_translate = QCoreApplication.translate
+
+
+class ReservoirTranslate(PamhyrTranslate):
+ def __init__(self):
+ super(ReservoirTranslate, self).__init__()
+
+ self._sub_dict["table_headers"] = {
+ "name": _translate("Reservoir", "Name"),
+ "node": _translate("Reservoir", "Node")
+ }
diff --git a/src/View/Reservoir/UndoCommand.py b/src/View/Reservoir/UndoCommand.py
new file mode 100644
index 00000000..078c31d3
--- /dev/null
+++ b/src/View/Reservoir/UndoCommand.py
@@ -0,0 +1,127 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+ QMessageBox, QUndoCommand, QUndoStack,
+)
+
+from Model.Reservoir.Reservoir import Reservoir
+from Model.Reservoir.ReservoirList import ReservoirList
+
+
+class SetNameCommand(QUndoCommand):
+ def __init__(self, reservoir_lst, index, new_value):
+ QUndoCommand.__init__(self)
+
+ self._reservoir_lst = reservoir_lst
+ self._index = index
+ self._old = self._reservoir_lst.get(self._index).name
+ self._new = str(new_value)
+
+ def undo(self):
+ self._reservoir_lst.get(self._index).name = self._old
+
+ def redo(self):
+ self._reservoir_lst.get(self._index).name = self._new
+
+
+class SetNodeCommand(QUndoCommand):
+ def __init__(self, reservoir_lst, index, node):
+ QUndoCommand.__init__(self)
+
+ self._reservoir_lst = reservoir_lst
+ self._index = index
+ self._old = self._reservoir_lst.get(self._index).node
+ self._new = node
+ self._prev_assoc_to_node = self._reservoir_lst.get_assoc_to_node(node)
+
+ def _previous_assoc_node(self, node):
+ if self._prev_assoc_to_node is not None:
+ self._prev_assoc_to_node.node = node
+
+ def undo(self):
+ self._reservoir_lst.get(self._index).node = self._old
+ self._previous_assoc_node(self._new)
+
+ def redo(self):
+ self._reservoir_lst.get(self._index).node = self._new
+ self._previous_assoc_node(None)
+
+
+class AddCommand(QUndoCommand):
+ def __init__(self, reservoir_lst, index):
+ QUndoCommand.__init__(self)
+
+ self._reservoir_lst = reservoir_lst
+
+ self._index = index
+ self._new = None
+
+ def undo(self):
+ self._reservoir_lst.delete_i([self._index])
+
+ def redo(self):
+ if self._new is None:
+ self._new = self._reservoir_lst.new(self._index)
+ else:
+ self._reservoir_lst.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+ def __init__(self, reservoir_lst, rows):
+ QUndoCommand.__init__(self)
+
+ self._reservoir_lst = reservoir_lst
+
+ self._rows = rows
+
+ self._reservoir = []
+ for row in rows:
+ self._reservoir.append((row, self._reservoir_lst.get(row)))
+ self._reservoir.sort()
+
+ def undo(self):
+ for row, el in self._reservoir:
+ self._reservoir_lst.insert(row, el)
+
+ def redo(self):
+ self._reservoir_lst.delete_i(self._rows)
+
+
+class PasteCommand(QUndoCommand):
+ def __init__(self, reservoir_lst, row, reservoir):
+ QUndoCommand.__init__(self)
+
+ self._reservoir_lst = reservoir_lst
+
+ self._row = row
+ self._reservoir = deepcopy(reservoir)
+ self._reservoir.reverse()
+
+ def undo(self):
+ self._reservoir_lst.delete_i(
+ self._tab,
+ range(self._row, self._row + len(self._reservoir))
+ )
+
+ def redo(self):
+ for r in self._reservoir:
+ self._reservoir_lst.insert(self._row, r)
diff --git a/src/View/Reservoir/Window.py b/src/View/Reservoir/Window.py
new file mode 100644
index 00000000..147c05d1
--- /dev/null
+++ b/src/View/Reservoir/Window.py
@@ -0,0 +1,173 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import trace, timer
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+
+from PyQt5.QtGui import (
+ QKeySequence,
+)
+
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel,
+ QCoreApplication, QModelIndex, pyqtSlot,
+ QRect,
+)
+
+from PyQt5.QtWidgets import (
+ QDialogButtonBox, QPushButton, QLineEdit,
+ QFileDialog, QTableView, QAbstractItemView,
+ QUndoStack, QShortcut, QAction, QItemDelegate,
+ QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
+)
+
+from View.Reservoir.Table import (
+ TableModel, ComboBoxDelegate
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.Reservoir.Translate import ReservoirTranslate
+from View.Reservoir.Edit.Window import EditReservoirWindow
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class ReservoirWindow(PamhyrWindow):
+ _pamhyr_ui = "ReservoirList"
+ _pamhyr_name = "Reservoir"
+
+ def __init__(self, study=None, config=None, parent=None):
+ name = self._pamhyr_name + " - " + study.name
+
+ super(ReservoirWindow, self).__init__(
+ title=name,
+ study=study,
+ config=config,
+ trad=ReservoirTranslate(),
+ parent=parent
+ )
+
+ self._reservoir_lst = self._study.river.reservoir
+
+ self.setup_table()
+ self.setup_graph()
+ self.setup_connections()
+
+ def setup_table(self):
+ self._table = None
+
+ self._delegate_node = ComboBoxDelegate(
+ trad=self._trad,
+ data=self._study.river,
+ parent=self
+ )
+
+ table = self.find(QTableView, f"tableView")
+ self._table = TableModel(
+ table_view=table,
+ table_headers=self._trad.get_dict("table_headers"),
+ editable_headers=["name", "node"],
+ delegates={
+ "node": self._delegate_node,
+ },
+ trad=self._trad,
+ data=self._study.river,
+ undo=self._undo_stack,
+ )
+
+ def setup_graph(self):
+ self.graph_widget = GraphWidget(
+ self._study.river,
+ min_size=None, size=(200, 200),
+ only_display=True,
+ parent=self
+ )
+ self.graph_layout = self.find(QVBoxLayout, "verticalLayout")
+ self.graph_layout.addWidget(self.graph_widget)
+
+ def setup_connections(self):
+ self.find(QAction, "action_add").triggered.connect(self.add)
+ self.find(QAction, "action_delete").triggered.connect(self.delete)
+ self.find(QAction, "action_edit").triggered.connect(self.edit)
+
+ def index_selected_row(self):
+ table = self.find(QTableView, "tableView")
+ return table.selectionModel()\
+ .selectedRows()[0]\
+ .row()
+
+ def index_selected_rows(self):
+ table = self.find(QTableView, "tableView")
+ return list(
+ # Delete duplicate
+ set(
+ map(
+ lambda i: i.row(),
+ table.selectedIndexes()
+ )
+ )
+ )
+
+ def add(self):
+ rows = self.index_selected_rows()
+ if len(self._reservoir_lst) == 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(self):
+ rows = self.index_selected_rows()
+ for row in rows:
+ data = self._reservoir_lst.get(row)
+
+ if self.sub_window_exists(
+ EditReservoirWindow,
+ data=[self._study, None, data]
+ ):
+ continue
+
+ win = EditReservoirWindow(
+ data=data,
+ study=self._study,
+ parent=self
+ )
+ win.show()
diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui
index f33e6dcf..9476708c 100644
--- a/src/View/ui/MainWindow.ui
+++ b/src/View/ui/MainWindow.ui
@@ -131,6 +131,7 @@
+
diff --git a/src/View/ui/Reservoir.ui b/src/View/ui/Reservoir.ui
new file mode 100644
index 00000000..d2dc3902
--- /dev/null
+++ b/src/View/ui/Reservoir.ui
@@ -0,0 +1,120 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 600
+
+
+
+ MainWindow
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
+ 300
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+
+ ressources/gtk-add.pngressources/gtk-add.png
+
+
+ Add
+
+
+ Add a new point
+
+
+
+
+
+ ressources/gtk-remove.pngressources/gtk-remove.png
+
+
+ Delete
+
+
+ Delete points
+
+
+
+
+
+ ressources/edit.pngressources/edit.png
+
+
+ Edit
+
+
+ Edit elevation/surface law
+
+
+
+
+
+ ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png
+
+
+ Sort
+
+
+ Sort points by elevation
+
+
+
+
+
+
diff --git a/src/View/ui/ReservoirList.ui b/src/View/ui/ReservoirList.ui
new file mode 100644
index 00000000..db3b822d
--- /dev/null
+++ b/src/View/ui/ReservoirList.ui
@@ -0,0 +1,114 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 849
+ 600
+
+
+
+
+ 300
+ 0
+
+
+
+ MainWindow
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
-
+
+
+
+ 300
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+
+ ressources/gtk-add.pngressources/gtk-add.png
+
+
+ Add
+
+
+ Add a new reservoir
+
+
+
+
+
+ ressources/gtk-remove.pngressources/gtk-remove.png
+
+
+ Delete
+
+
+ Delete reservoirs
+
+
+
+
+
+ ressources/edit.pngressources/edit.png
+
+
+ Edit
+
+
+ Edit reservoir law
+
+
+
+
+
+