add Reservoirs

setup.py
Theophile Terraz 2023-11-29 14:34:06 +01:00
parent 12e3d779c6
commit 92eb729a39
18 changed files with 1791 additions and 11 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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()

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrList import PamhyrModelList
from Model.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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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,

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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()

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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()

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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)"),
}

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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)

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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()

170
src/View/Reservoir/Table.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.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()

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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")
}

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.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)

View File

@ -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 <https://www.gnu.org/licenses/>.
# -*- 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()

View File

@ -131,6 +131,7 @@
<addaction name="action_menu_initial_conditions"/>
<addaction name="action_menu_edit_friction"/>
<addaction name="action_menu_edit_lateral_contribution"/>
<addaction name="action_menu_edit_reservoirs"/>
</widget>
<widget class="QMenu" name="menu_results">
<property name="title">
@ -492,10 +493,7 @@
<string>Boundary conditions and one-time contributions</string>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
<font/>
</property>
</action>
<action name="action_menu_initial_conditions">
@ -600,11 +598,7 @@
<string>Visualize the last results</string>
</property>
<property name="font">
<font>
<family>Ubuntu</family>
<weight>50</weight>
<bold>false</bold>
</font>
<font/>
</property>
</action>
<action name="action_plot_limnigram">
@ -932,6 +926,14 @@
<string>Developers (html)</string>
</property>
</action>
<action name="action_menu_edit_reservoirs">
<property name="text">
<string>Reservoirs</string>
</property>
<property name="toolTip">
<string>Edit reservoirs</string>
</property>
</action>
</widget>
<resources/>
<connections>

120
src/View/ui/Reservoir.ui Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QTableView" name="tableView">
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_delete"/>
<addaction name="action_sort"/>
</widget>
<action name="action_add">
<property name="icon">
<iconset>
<normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add a new point</string>
</property>
</action>
<action name="action_delete">
<property name="icon">
<iconset>
<normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete points</string>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset>
<normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="toolTip">
<string>Edit elevation/surface law</string>
</property>
</action>
<action name="action_sort">
<property name="icon">
<iconset>
<normaloff>ressources/gtk-sort-ascending.png</normaloff>ressources/gtk-sort-ascending.png</iconset>
</property>
<property name="text">
<string>Sort</string>
</property>
<property name="toolTip">
<string>Sort points by elevation</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>849</width>
<height>600</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="_1">
<item>
<widget class="QTableView" name="tableView">
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>849</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_delete"/>
<addaction name="action_edit"/>
</widget>
<action name="action_add">
<property name="icon">
<iconset>
<normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add a new reservoir</string>
</property>
</action>
<action name="action_delete">
<property name="icon">
<iconset>
<normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete reservoirs</string>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset>
<normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="toolTip">
<string>Edit reservoir law</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>