diff --git a/src/Model/OutputKpAdists/OutputKpAdists.py b/src/Model/OutputKpAdists/OutputKpAdists.py
index 75520c4d..af1c6df0 100644
--- a/src/Model/OutputKpAdists/OutputKpAdists.py
+++ b/src/Model/OutputKpAdists/OutputKpAdists.py
@@ -135,7 +135,7 @@ class OutputKpAdists(SQLSubModel):
"OutputKpAdists(id, reach, kp, title " +
"VALUES (" +
f"{self.id}, {self._reach}, " +
- f"{self._kp}, '{self._db_format(self._title)}" +
+ f"{self._kp}, '{self._db_format(self._title)}'" +
")"
)
diff --git a/src/Model/Pollutants/Pollutants.py b/src/Model/Pollutants/Pollutants.py
new file mode 100644
index 00000000..958a0d49
--- /dev/null
+++ b/src/Model/Pollutants/Pollutants.py
@@ -0,0 +1,131 @@
+# Pollutants.py -- Pamhyr
+# Copyright (C) 2023-2024 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,
+ old_pamhyr_date_to_timestamp,
+ date_iso_to_timestamp,
+ date_dmy_to_timestamp,
+)
+
+from Model.Tools.PamhyrDB import SQLSubModel
+from Model.Except import NotImplementedMethodeError
+
+logger = logging.getLogger()
+
+class Pollutants(SQLSubModel):
+ _sub_classes = []
+ _id_cnt = 0
+
+ def __init__(self, id: int = -1, name: str = "", status=None):
+ super(Pollutants, self).__init__()
+
+ self._status = status
+
+ if id == -1:
+ self.id = Pollutants._id_cnt
+ else:
+ self.id = id
+
+ self._name = str(name)
+ self._enabled = True
+
+ Pollutants._id_cnt = max(
+ Pollutants._id_cnt + 1, self.id)
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, name):
+ self._name = name
+ self._status.modified()
+
+
+ @classmethod
+ def _db_create(cls, execute):
+ execute("""
+ CREATE TABLE Pollutants(
+ id INTEGER NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE
+ )
+ """)
+
+ return cls._create_submodel(execute)
+
+ @classmethod
+ def _db_update(cls, execute, version):
+ return True
+
+ @classmethod
+ def _db_load(cls, execute, data=None):
+ new = []
+
+ status = data["status"]
+
+ table = execute(
+ "SELECT id, name " +
+ f"FROM Pollutants"
+ )
+
+ if table is not None:
+ for row in table:
+ id = row[0]
+ name = row[1]
+
+ new_pollutant = cls(
+ id=id, name=name,
+ status=status
+ )
+
+ new.append(new_pollutant)
+
+ return new
+
+ def _db_save(self, execute, data=None):
+
+ sql = (
+ "INSERT INTO " +
+ "Pollutants(id, name " +
+ "VALUES (" +
+ f"{self.id}, " +
+ f"'{self._db_format(self._name)}'" +
+ ")"
+ )
+
+ execute(sql)
+
+ return True
+
+ @property
+ def enabled(self):
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, enabled):
+ self._enabled = enabled
+ self._status.modified()
+
+
+
+
+
+
diff --git a/src/Model/Pollutants/PollutantsList.py b/src/Model/Pollutants/PollutantsList.py
new file mode 100644
index 00000000..2526329a
--- /dev/null
+++ b/src/Model/Pollutants/PollutantsList.py
@@ -0,0 +1,56 @@
+# PollutantsList.py -- Pamhyr
+# Copyright (C) 2024 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 tools import trace, timer
+
+from Model.Except import NotImplementedMethodeError
+from Model.Tools.PamhyrList import PamhyrModelList
+from Model.Pollutants.Pollutants import Pollutants
+
+
+class PollutantsList(PamhyrModelList):
+ _sub_classes = [Pollutants]
+
+ @classmethod
+ def _db_load(cls, execute, data=None):
+ new = cls(status=data["status"])
+
+ new._lst = Pollutants._db_load(execute, data)
+
+ return new
+
+ def _db_save(self, execute, data=None):
+ ok = True
+
+ # Delete previous data
+ execute("DELETE FROM Pollutants")
+
+ for sl in self._lst:
+ ok &= sl._db_save(execute, data)
+
+ return ok
+
+ @property
+ def Pollutants_List(self):
+ return self.lst
+
+ def new(self, lst, index):
+ n = Pollutants(status=self._status)
+ self._lst.insert(index, n)
+ self._status.modified()
+ return n
diff --git a/src/Model/River.py b/src/Model/River.py
index cb414179..066dbaaf 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -46,6 +46,7 @@ from Model.REPLine.REPLineList import REPLineList
from Solver.Solvers import solver_type_list
from Model.OutputKpAdists.OutputKpListAdists import OutputKpAdistsList
+from Model.Pollutants.PollutantsList import PollutantsList
class RiverNode(Node, SQLSubModel):
@@ -255,6 +256,7 @@ class River(Graph, SQLSubModel):
self._additional_files = AddFileList(status=self._status)
self._rep_lines = REPLineList(status=self._status)
self._Output_kp_adists = OutputKpAdistsList(status=self._status)
+ self._Pollutants = PollutantsList(status=self._status)
@classmethod
def _db_create(cls, execute):
@@ -333,6 +335,8 @@ class River(Graph, SQLSubModel):
execute, data
)
+ new._Pollutants = PollutantsList._db_load(execute, data)
+
return new
def _db_save(self, execute, data=None):
@@ -353,6 +357,7 @@ class River(Graph, SQLSubModel):
objs.append(self._parameters[solver])
objs.append(self._Output_kp_adists)
+ objs.append(self._Pollutants)
self._save_submodel(execute, objs, data)
return True
@@ -480,6 +485,10 @@ Last export at: @date."""
def Output_kp_adists(self):
return self._Output_kp_adists
+ @property
+ def Pollutants(self):
+ return self._Pollutants
+
def get_params(self, solver):
if solver in self._parameters:
return self._parameters[solver]
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 01d08e5b..f94c4d07 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -76,6 +76,7 @@ from View.Results.Window import ResultsWindow
from View.Results.ReadingResultsDialog import ReadingResultsDialog
from View.Debug.Window import ReplWindow
from View.OutputKpAdisTS.Window import OutputKpAdisTSWindow
+from View.Pollutants.Window import PollutantsWindow
# Optional internal display of documentation for make the application
# package lighter...
@@ -120,7 +121,7 @@ define_model_action = [
"action_menu_results_last", "action_open_results_from_file",
"action_menu_boundary_conditions_sediment",
"action_menu_rep_additional_lines", "action_menu_output_kp",
- "action_menu_run_adists",
+ "action_menu_run_adists", "action_menu_pollutants",
]
action = (
@@ -234,6 +235,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"""
actions = {
# Menu action
+ "action_menu_pollutants": self.open_pollutants,
"action_menu_run_adists":self.run_solver_adists,
"action_menu_output_kp": self.open_output_kp_adists,
"action_menu_config": self.open_configure,
@@ -864,6 +866,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# SUB WINDOWS #
###############
+ def open_pollutants(self):
+ if self.sub_window_exists(
+ PollutantsWindow,
+ data=[self._study, None]
+ ):
+ return
+
+ Pollutants = PollutantsWindow(
+ study=self._study,
+ parent=self
+ )
+ Pollutants.show()
+
def open_output_kp_adists(self):
if self.sub_window_exists(
OutputKpAdisTSWindow,
diff --git a/src/View/Pollutants/BasicHydraulicStructures/Table.py b/src/View/Pollutants/BasicHydraulicStructures/Table.py
new file mode 100644
index 00000000..f69a4099
--- /dev/null
+++ b/src/View/Pollutants/BasicHydraulicStructures/Table.py
@@ -0,0 +1,277 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023-2024 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, QMessageBox,
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from View.HydraulicStructures.BasicHydraulicStructures.UndoCommand import (
+ SetNameCommand, SetTypeCommand,
+ SetEnabledCommand, AddCommand, DelCommand,
+ SetValueCommand,
+)
+from Model.HydraulicStructures.Basic.Types import BHS_types
+
+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
+
+ self._long_types = {}
+ if self._trad is not None:
+ self._long_types = self._trad.get_dict("long_types")
+
+ def createEditor(self, parent, option, index):
+ self.editor = QComboBox(parent)
+
+ lst = list(
+ map(
+ lambda k: self._long_types[k],
+ BHS_types.keys()
+ )
+ )
+ self.editor.addItems(lst)
+
+ 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 __init__(self, trad=None, **kwargs):
+ self._trad = trad
+ self._long_types = {}
+ if self._trad is not None:
+ self._long_types = self._trad.get_dict("long_types")
+
+ super(TableModel, self).__init__(trad=trad, **kwargs)
+
+ 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._data.basic_structure(row).name
+ elif self._headers[column] == "type":
+ return self._long_types[self._data.basic_structure(row).type]
+
+ 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._data, row, value
+ )
+ )
+ elif self._headers[column] == "type":
+ old_type = self._data.basic_structure(row).type
+
+ if old_type == "ND" or self._question_set_type():
+ key = next(
+ k for k, v in self._long_types.items()
+ if v == value
+ )
+
+ self._undo.push(
+ SetTypeCommand(
+ self._data, row, BHS_types[key]
+ )
+ )
+ except Exception as e:
+ logger.error(e)
+ logger.debug(traceback.format_exc())
+
+ self.dataChanged.emit(index, index)
+ return True
+
+ def _question_set_type(self):
+ question = QMessageBox(self._parent)
+
+ question.setWindowTitle(self._trad['msg_type_change_title'])
+ question.setText(self._trad['msg_type_change_text'])
+ question.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
+ question.setIcon(QMessageBox.Question)
+
+ res = question.exec()
+ return res == QMessageBox.Ok
+
+ 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()
+ self.layoutChanged.emit()
+
+ def enabled(self, row, enabled, parent=QModelIndex()):
+ self._undo.push(
+ SetEnabledCommand(
+ self._lst, row, enabled
+ )
+ )
+ self.layoutChanged.emit()
+
+ def undo(self):
+ self._undo.undo()
+ self.layoutChanged.emit()
+
+ def redo(self):
+ self._undo.redo()
+ self.layoutChanged.emit()
+
+
+class ParametersTableModel(PamhyrTableModel):
+ def __init__(self, trad=None, **kwargs):
+ self._trad = trad
+ self._long_types = {}
+
+ if self._trad is not None:
+ self._long_types = self._trad.get_dict("long_types")
+
+ self._hs_index = None
+
+ super(ParametersTableModel, self).__init__(trad=trad, **kwargs)
+
+ def rowCount(self, parent):
+ if self._hs_index is None:
+ return 0
+
+ return len(
+ self._data.basic_structure(self._hs_index)
+ )
+
+ def data(self, index, role):
+ if role != Qt.ItemDataRole.DisplayRole:
+ return QVariant()
+
+ if self._hs_index is None:
+ return QVariant()
+
+ row = index.row()
+ column = index.column()
+
+ hs = self._data.basic_structure(self._hs_index)
+
+ if self._headers[column] == "name":
+ return self._trad[hs.parameters[row].name]
+ elif self._headers[column] == "value":
+ return str(hs.parameters[row].value)
+
+ return QVariant()
+
+ def setData(self, index, value, role=Qt.EditRole):
+ if not index.isValid() or role != Qt.EditRole:
+ return False
+
+ if self._hs_index is None:
+ return QVariant()
+
+ row = index.row()
+ column = index.column()
+
+ try:
+ if self._headers[column] == "value":
+ self._undo.push(
+ SetValueCommand(
+ self._data.basic_structure(self._hs_index),
+ row, value
+ )
+ )
+ except Exception as e:
+ logger.error(e)
+ logger.debug(traceback.format_exc())
+
+ self.dataChanged.emit(index, index)
+ return True
+
+ def update_hs_index(self, index):
+ self._hs_index = index
+ self.layoutChanged.emit()
diff --git a/src/View/Pollutants/BasicHydraulicStructures/Translate.py b/src/View/Pollutants/BasicHydraulicStructures/Translate.py
new file mode 100644
index 00000000..f55fb2d7
--- /dev/null
+++ b/src/View/Pollutants/BasicHydraulicStructures/Translate.py
@@ -0,0 +1,155 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023-2024 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.Translate import MainTranslate
+
+_translate = QCoreApplication.translate
+
+
+class BasicHydraulicStructuresTranslate(MainTranslate):
+ def __init__(self):
+ super(BasicHydraulicStructuresTranslate, self).__init__()
+
+ self._dict["Basic Hydraulic Structures"] = _translate(
+ "BasicHydraulicStructures", "Basic Hydraulic Structures"
+ )
+
+ self._dict['msg_type_change_title'] = _translate(
+ "BasicHydraulicStructures",
+ "Change hydraulic structure type"
+ )
+
+ self._dict['msg_type_change_text'] = _translate(
+ "BasicHydraulicStructures",
+ "Do you want to change the hydraulic structure type and reset \
+hydraulic structure values?"
+ )
+
+ # BHSValues translation
+
+ self._dict['width'] = self._dict["unit_width"]
+ self._dict['height'] = self._dict["unit_thickness"]
+ self._dict['elevation'] = self._dict["unit_elevation"]
+ self._dict['diameter'] = self._dict["unit_diameter"]
+ self._dict['discharge_coefficient'] = _translate(
+ "BasicHydraulicStructures", "Discharge coefficient"
+ )
+ self._dict['loading_elevation'] = _translate(
+ "BasicHydraulicStructures", "Upper elevation (m)"
+ )
+ self._dict['half-angle_tangent'] = _translate(
+ "BasicHydraulicStructures", "Half-angle tangent"
+ )
+ self._dict['maximal_loading_elevation'] = _translate(
+ "BasicHydraulicStructures", "Maximal loading elevation"
+ )
+ self._dict['siltation_height'] = _translate(
+ "BasicHydraulicStructures", "Siltation height (m)"
+ )
+ self._dict['top_of_the_vault'] = _translate(
+ "BasicHydraulicStructures", "Top of the vault (m)"
+ )
+ self._dict['bottom_of_the_vault'] = _translate(
+ "BasicHydraulicStructures", "Bottom of the vault (m)"
+ )
+ self._dict['opening'] = _translate(
+ "BasicHydraulicStructures", "Opening"
+ )
+ self._dict['maximal_opening'] = _translate(
+ "BasicHydraulicStructures", "Maximal opening"
+ )
+ self._dict['step_space'] = _translate(
+ "BasicHydraulicStructures", "Step space"
+ )
+ self._dict['weir'] = _translate(
+ "BasicHydraulicStructures", "Weir"
+ )
+ self._dict['coefficient'] = _translate(
+ "BasicHydraulicStructures", "Coefficient"
+ )
+
+ # Dummy parameters
+
+ self._dict['parameter_1'] = _translate(
+ "BasicHydraulicStructures", "Parameter 1"
+ )
+ self._dict['parameter_2'] = _translate(
+ "BasicHydraulicStructures", "Parameter 2"
+ )
+ self._dict['parameter_3'] = _translate(
+ "BasicHydraulicStructures", "Parameter 3"
+ )
+ self._dict['parameter_4'] = _translate(
+ "BasicHydraulicStructures", "Parameter 4"
+ )
+ self._dict['parameter_5'] = _translate(
+ "BasicHydraulicStructures", "Parameter 5"
+ )
+
+ # BHS types long names
+
+ self._sub_dict["long_types"] = {
+ "ND": self._dict["not_defined"],
+ "S1": _translate(
+ "BasicHydraulicStructures", "Discharge weir"
+ ),
+ "S2": _translate(
+ "BasicHydraulicStructures", "Trapezoidal weir"
+ ),
+ "S3": _translate(
+ "BasicHydraulicStructures", "Triangular weir"
+ ),
+ "OR": _translate(
+ "BasicHydraulicStructures", "Rectangular orifice"
+ ),
+ "OC": _translate(
+ "BasicHydraulicStructures", "Circular orifice"
+ ),
+ "OV": _translate(
+ "BasicHydraulicStructures", "Vaulted orifice"
+ ),
+ "V1": _translate(
+ "BasicHydraulicStructures", "Rectangular gate"
+ ),
+ "V2": _translate(
+ "BasicHydraulicStructures", "Simplified rectangular gate"
+ ),
+ "BO": _translate(
+ "BasicHydraulicStructures", "Borda-type head loss"
+ ),
+ "CV": _translate(
+ "BasicHydraulicStructures", "Check valve"
+ ),
+ "UD": _translate(
+ "BasicHydraulicStructures", "User defined"
+ ),
+ }
+
+ # Tables
+
+ self._sub_dict["table_headers"] = {
+ "name": self._dict["name"],
+ "type": self._dict["type"],
+ }
+
+ self._sub_dict["table_headers_parameters"] = {
+ "name": self._dict["name"],
+ "value": self._dict["value"],
+ }
diff --git a/src/View/Pollutants/BasicHydraulicStructures/UndoCommand.py b/src/View/Pollutants/BasicHydraulicStructures/UndoCommand.py
new file mode 100644
index 00000000..78fa3d62
--- /dev/null
+++ b/src/View/Pollutants/BasicHydraulicStructures/UndoCommand.py
@@ -0,0 +1,153 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023-2024 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,
+)
+
+
+class SetNameCommand(QUndoCommand):
+ def __init__(self, hs, index, new_value):
+ QUndoCommand.__init__(self)
+
+ self._hs = hs
+ self._index = index
+ self._old = self._hs.basic_structure(self._index).name
+ self._new = str(new_value)
+
+ def undo(self):
+ self._hs.basic_structure(self._index).name = self._old
+
+ def redo(self):
+ self._hs.basic_structure(self._index).name = self._new
+
+
+class SetTypeCommand(QUndoCommand):
+ def __init__(self, hs, index, new_type):
+ QUndoCommand.__init__(self)
+
+ self._hs = hs
+ self._index = index
+ self._type = new_type
+ self._old = self._hs.basic_structure(self._index)
+ self._new = self._hs.basic_structure(self._index)\
+ .convert(self._type)
+
+ def undo(self):
+ self._hs.delete_i([self._index])
+ self._hs.insert(self._index, self._old)
+
+ def redo(self):
+ self._hs.delete_i([self._index])
+ self._hs.insert(self._index, self._new)
+
+
+class SetEnabledCommand(QUndoCommand):
+ def __init__(self, hs, index, enabled):
+ QUndoCommand.__init__(self)
+
+ self._hs = hs
+ self._index = index
+ self._old = not enabled
+ self._new = enabled
+
+ def undo(self):
+ self._hs.basic_structure(self._index).enabled = self._old
+
+ def redo(self):
+ self._hs.basic_structure(self._index).enabled = self._new
+
+
+class AddCommand(QUndoCommand):
+ def __init__(self, hs, index):
+ QUndoCommand.__init__(self)
+
+ self._hs = hs
+
+ self._index = index
+ self._new = None
+
+ def undo(self):
+ self._hs.delete_i([self._index])
+
+ def redo(self):
+ if self._new is None:
+ self._new = self._hs.add(self._index)
+ else:
+ self._hs.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+ def __init__(self, hs, rows):
+ QUndoCommand.__init__(self)
+
+ self._hs = hs
+
+ self._rows = rows
+
+ self._bhs = []
+ for row in rows:
+ self._bhs.append((row, self._hs.basic_structure(row)))
+
+ def undo(self):
+ for row, el in self._bhs:
+ self._hs.insert(row, el)
+
+ def redo(self):
+ self._hs.delete_i(self._rows)
+
+
+class PasteCommand(QUndoCommand):
+ def __init__(self, hs, row, h_s):
+ QUndoCommand.__init__(self)
+
+ self._hs = hs
+
+ self._row = row
+ self._bhs = deepcopy(h_s)
+ self._bhs.reverse()
+
+ def undo(self):
+ self._hs.delete_i(range(self._row, self._row + len(self._bhs)))
+
+ def redo(self):
+ for r in self._bhs:
+ self._hs.insert(self._row, r)
+
+####################################
+# Basic hydraulic structure values #
+####################################
+
+
+class SetValueCommand(QUndoCommand):
+ def __init__(self, bhs, index, value):
+ QUndoCommand.__init__(self)
+
+ self._bhs = bhs
+ self._index = index
+ self._old = self._bhs.parameters[self._index].value
+ self._new = self._bhs.parameters[self._index].type(value)
+
+ def undo(self):
+ self._bhs.parameters[self._index].value = self._old
+
+ def redo(self):
+ self._bhs.parameters[self._index].value = self._new
diff --git a/src/View/Pollutants/BasicHydraulicStructures/Window.py b/src/View/Pollutants/BasicHydraulicStructures/Window.py
new file mode 100644
index 00000000..9a39f49c
--- /dev/null
+++ b/src/View/Pollutants/BasicHydraulicStructures/Window.py
@@ -0,0 +1,270 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023-2024 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 PyQt5 import QtCore
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel, QCoreApplication,
+ pyqtSlot, pyqtSignal, QItemSelectionModel,
+)
+
+from PyQt5.QtWidgets import (
+ QDialogButtonBox, QPushButton, QLineEdit,
+ QFileDialog, QTableView, QAbstractItemView,
+ QUndoStack, QShortcut, QAction, QItemDelegate,
+ QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.HydraulicStructures.PlotAC import PlotAC
+
+from View.HydraulicStructures.BasicHydraulicStructures.Table import (
+ ComboBoxDelegate, TableModel, ParametersTableModel,
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.HydraulicStructures.BasicHydraulicStructures.Translate import (
+ BasicHydraulicStructuresTranslate
+)
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class BasicHydraulicStructuresWindow(PamhyrWindow):
+ _pamhyr_ui = "BasicHydraulicStructures"
+ _pamhyr_name = "Basic Hydraulic Structures"
+
+ def __init__(self, data=None, study=None, config=None, parent=None):
+ trad = BasicHydraulicStructuresTranslate()
+ name = " - ".join([
+ trad[self._pamhyr_name], data.name, study.name
+ ])
+
+ super(BasicHydraulicStructuresWindow, self).__init__(
+ title=name,
+ study=study,
+ config=config,
+ trad=trad,
+ parent=parent
+ )
+
+ self._hash_data.append(data)
+
+ self._hs = data
+
+ self.setup_table()
+ self.setup_checkbox()
+ self.setup_plot()
+ self.setup_connections()
+
+ self.update()
+
+ def setup_table(self):
+ self.setup_table_bhs()
+ self.setup_table_bhs_parameters()
+
+ def setup_table_bhs(self):
+ self._table = None
+
+ self._delegate_type = ComboBoxDelegate(
+ trad=self._trad,
+ 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", "type"],
+ delegates={
+ "type": self._delegate_type,
+ },
+ trad=self._trad,
+ data=self._hs,
+ undo=self._undo_stack,
+ parent=self,
+ )
+
+ selectionModel = table.selectionModel()
+ index = table.model().index(0, 0)
+
+ selectionModel.select(
+ index,
+ QItemSelectionModel.Rows |
+ QItemSelectionModel.ClearAndSelect |
+ QItemSelectionModel.Select
+ )
+ table.scrollTo(index)
+
+ def setup_table_bhs_parameters(self):
+ self._table_parameters = None
+
+ table = self.find(QTableView, f"tableView_2")
+ self._table_parameters = ParametersTableModel(
+ table_view=table,
+ table_headers=self._trad.get_dict("table_headers_parameters"),
+ editable_headers=["value"],
+ delegates={},
+ trad=self._trad,
+ data=self._hs,
+ undo=self._undo_stack,
+ parent=self,
+ )
+
+ def setup_checkbox(self):
+ self._checkbox = self.find(QCheckBox, f"checkBox")
+ self._set_checkbox_state()
+
+ def setup_plot(self):
+ self.canvas = MplCanvas(width=5, height=4, dpi=100)
+ self.canvas.setObjectName("canvas")
+ self.toolbar = PamhyrPlotToolbar(
+ self.canvas, self
+ )
+ self.plot_layout = self.find(QVBoxLayout, "verticalLayout")
+ self.plot_layout.addWidget(self.toolbar)
+ self.plot_layout.addWidget(self.canvas)
+
+ reach = self._hs.input_reach
+ profile_kp = self._hs.input_kp
+ if profile_kp is not None:
+ profiles = reach.reach.get_profiles_from_kp(float(profile_kp))
+ else:
+ profiles = None
+ if profiles is not None:
+ profile = profiles[0]
+ else:
+ profile = None
+
+ self.plot_ac = PlotAC(
+ canvas=self.canvas,
+ river=self._study.river,
+ reach=self._hs.input_reach,
+ profile=profile,
+ trad=self._trad,
+ toolbar=self.toolbar
+ )
+ self.plot_ac.draw()
+
+ def setup_connections(self):
+ self.find(QAction, "action_add").triggered.connect(self.add)
+ self.find(QAction, "action_delete").triggered.connect(self.delete)
+ self._checkbox.clicked.connect(self._set_basic_structure_state)
+
+ table = self.find(QTableView, "tableView")
+ table.selectionModel()\
+ .selectionChanged\
+ .connect(self.update)
+
+ self._table.dataChanged.connect(self.update)
+ self._table.layoutChanged.connect(self.update)
+
+ def index_selected(self):
+ table = self.find(QTableView, "tableView")
+ r = table.selectionModel().selectedRows()
+
+ if len(r) > 0:
+ return r[0]
+ else:
+ return None
+
+ def index_selected_row(self):
+ table = self.find(QTableView, "tableView")
+ r = table.selectionModel().selectedRows()
+
+ if len(r) > 0:
+ return r[0].row()
+ else:
+ return None
+
+ 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._hs) == 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 _set_checkbox_state(self):
+ row = self.index_selected_row()
+
+ if row is None:
+ self._checkbox.setEnabled(False)
+ self._checkbox.setChecked(True)
+ else:
+ self._checkbox.setEnabled(True)
+ self._checkbox.setChecked(self._hs.basic_structure(row).enabled)
+
+ def _set_basic_structure_state(self):
+ rows = self.index_selected_rows()
+ if len(rows) != 0:
+ for row in rows:
+ if row is not None:
+ self._table.enabled(
+ row,
+ self._checkbox.isChecked()
+ )
+
+ def update(self):
+ self._set_checkbox_state()
+ self._update_parameters_table()
+
+ def _update_parameters_table(self):
+ row = self.index_selected_row()
+ self._table_parameters.update_hs_index(row)
diff --git a/src/View/Pollutants/PlotAC.py b/src/View/Pollutants/PlotAC.py
new file mode 100644
index 00000000..c63dc9bf
--- /dev/null
+++ b/src/View/Pollutants/PlotAC.py
@@ -0,0 +1,120 @@
+# PlotAC.py -- Pamhyr
+# Copyright (C) 2023-2024 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 tools import timer
+from View.Tools.PamhyrPlot import PamhyrPlot
+from matplotlib import pyplot as plt
+
+
+class PlotAC(PamhyrPlot):
+ def __init__(self, canvas=None, trad=None, toolbar=None,
+ river=None, reach=None, profile=None,
+ parent=None):
+ super(PlotAC, self).__init__(
+ canvas=canvas,
+ trad=trad,
+ data=river,
+ toolbar=toolbar,
+ parent=parent
+ )
+
+ self._current_reach = reach
+ self._current_profile = profile
+
+ self.label_x = self._trad["x"]
+ self.label_y = self._trad["unit_elevation"]
+
+ self._isometric_axis = False
+
+ self._auto_relim_update = True
+ self._autoscale_update = True
+
+ @property
+ def river(self):
+ return self.data
+
+ @river.setter
+ def river(self, river):
+ self.data = river
+
+ @timer
+ def draw(self):
+ self.init_axes()
+
+ if self.data is None:
+ self.line_kp = None
+ return
+
+ if self._current_reach is None:
+ self.line_kp = None
+ return
+
+ self.draw_data()
+
+ self.idle()
+ self._init = True
+
+ def draw_data(self):
+ reach = self._current_reach
+
+ if self._current_profile is None:
+ self.line_kp = None
+ else:
+ profile = self._current_profile
+ x = profile.get_station()
+ z = profile.z()
+
+ self.line_kp, = self.canvas.axes.plot(
+ x, z,
+ color=self.color_plot_river_bottom,
+ **self.plot_default_kargs
+ )
+
+ def set_reach(self, reach):
+ self._current_reach = reach
+ self.update()
+
+ def set_profile(self, profile):
+ self._current_profile = profile
+ self.update()
+
+ def update(self):
+ if self.line_kp is None:
+ self.draw()
+ return
+
+ if self._current_reach is None or self._current_profile is None:
+ self.update_clear()
+ else:
+ self.update_data()
+
+ self.update_idle()
+
+ def update_data(self):
+ profile = self._current_profile
+ x = profile.get_station()
+ z = profile.z()
+
+ self.line_kp.set_data(x, z)
+
+ def clear(self):
+ self.update_clear()
+
+ def update_clear(self):
+ if self.line_kp is not None:
+ self.line_kp.set_data([], [])
diff --git a/src/View/Pollutants/PlotKPC.py b/src/View/Pollutants/PlotKPC.py
new file mode 100644
index 00000000..3327f6e6
--- /dev/null
+++ b/src/View/Pollutants/PlotKPC.py
@@ -0,0 +1,165 @@
+# PlotKPC.py -- Pamhyr
+# Copyright (C) 2023-2024 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 tools import timer
+from View.Tools.PamhyrPlot import PamhyrPlot
+
+from PyQt5.QtCore import (
+ QCoreApplication
+)
+
+from matplotlib.collections import LineCollection
+
+_translate = QCoreApplication.translate
+
+
+class PlotKPC(PamhyrPlot):
+ def __init__(self, canvas=None, trad=None, toolbar=None,
+ river=None, reach=None, profile=None,
+ parent=None):
+ super(PlotKPC, self).__init__(
+ canvas=canvas,
+ trad=trad,
+ data=river,
+ toolbar=toolbar,
+ parent=parent
+ )
+
+ self._current_reach = reach
+ self._current_profile = profile
+
+ self.label_x = self._trad["unit_kp"]
+ self.label_y = self._trad["unit_elevation"]
+
+ self._isometric_axis = False
+
+ self._auto_relim_update = True
+ self._autoscale_update = True
+
+ @property
+ def river(self):
+ return self.data
+
+ @river.setter
+ def river(self, river):
+ self.data = river
+
+ @timer
+ def draw(self, highlight=None):
+ self.init_axes()
+
+ if self.data is None:
+ self.profile = None
+ self.line_kp_zmin_zmax = None
+ self.line_kp_zmin = None
+ return
+
+ if self._current_reach is None:
+ self.profile = None
+ self.line_kp_zmin_zmax = None
+ self.line_kp_zmin = None
+ return
+
+ self.draw_data()
+ self.draw_current()
+
+ self.idle()
+ self._init = True
+
+ def draw_data(self):
+ reach = self._current_reach
+
+ kp = reach.reach.get_kp()
+ z_min = reach.reach.get_z_min()
+ z_max = reach.reach.get_z_max()
+
+ self.line_kp_zmin, = self.canvas.axes.plot(
+ kp, z_min,
+ color=self.color_plot_river_bottom,
+ lw=1.
+ )
+
+ if len(kp) != 0:
+ self.line_kp_zmin_zmax = self.canvas.axes.vlines(
+ x=kp,
+ ymin=z_min, ymax=z_max,
+ color=self.color_plot,
+ lw=1.
+ )
+
+ def draw_current(self):
+ if self._current_profile is None:
+ self.profile = None
+ else:
+ kp = [self._current_profile.kp,
+ self._current_profile.kp]
+ min_max = [self._current_profile.z_min(),
+ self._current_profile.z_max()]
+
+ self.profile = self.canvas.axes.plot(
+ kp, min_max,
+ color=self.color_plot_current,
+ lw=1.
+ )
+
+ def set_reach(self, reach):
+ self._current_reach = reach
+ self._current_profile = None
+ self.update()
+
+ def set_profile(self, profile):
+ self._current_profile = profile
+ self.update_current_profile()
+
+ def update(self):
+ self.draw()
+
+ def update_current_profile(self):
+ reach = self._current_reach
+ kp = reach.reach.get_kp()
+ z_min = reach.reach.get_z_min()
+ z_max = reach.reach.get_z_max()
+
+ if self.profile is None:
+ self.draw()
+ else:
+ self.profile.set_data(
+ [self._current_profile.kp, self._current_profile.kp],
+ [self._current_profile.z_min(), self._current_profile.z_max()],
+ )
+
+ self.update_idle()
+
+ def clear(self):
+ if self.profile is not None:
+ self.profile[0].set_data([], [])
+
+ if self.line_kp_zmin_zmax is not None:
+ self.line_kp_zmin_zmax.remove()
+ self.line_kp_zmin_zmax = None
+
+ if self.line_kp_zmin is not None:
+ self.line_kp_zmin.set_data([], [])
+
+ self.canvas.figure.canvas.draw_idle()
+
+ def clear_profile(self):
+ if self.profile is not None:
+ self.profile.set_data([], [])
+
+ self.canvas.figure.canvas.draw_idle()
diff --git a/src/View/Pollutants/Table.py b/src/View/Pollutants/Table.py
new file mode 100644
index 00000000..1a92742c
--- /dev/null
+++ b/src/View/Pollutants/Table.py
@@ -0,0 +1,126 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023-2024 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.Pollutants.UndoCommand import (
+ SetNameCommand,
+ SetEnabledCommand, AddCommand, DelCommand,
+)
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+class TableModel(PamhyrTableModel):
+ def _setup_lst(self):
+ self._lst = self._data._Pollutants
+
+ 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
+
+ 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
+ )
+ )
+ 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 enabled(self, row, enabled, parent=QModelIndex()):
+ self._undo.push(
+ SetEnabledCommand(
+ self._lst, row, enabled
+ )
+ )
+ 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/Pollutants/Translate.py b/src/View/Pollutants/Translate.py
new file mode 100644
index 00000000..ed2c9fc3
--- /dev/null
+++ b/src/View/Pollutants/Translate.py
@@ -0,0 +1,38 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023-2024 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.Translate import MainTranslate
+
+_translate = QCoreApplication.translate
+
+
+class PollutantsTranslate(MainTranslate):
+ def __init__(self):
+ super(PollutantsTranslate, self).__init__()
+
+ self._dict["Pollutants"] = _translate(
+ "Pollutants", "Pollutants"
+ )
+
+ self._dict["x"] = _translate("Pollutants", "X (m)")
+
+ self._sub_dict["table_headers"] = {
+ "name": self._dict["name"],
+ }
diff --git a/src/View/Pollutants/UndoCommand.py b/src/View/Pollutants/UndoCommand.py
new file mode 100644
index 00000000..213bf020
--- /dev/null
+++ b/src/View/Pollutants/UndoCommand.py
@@ -0,0 +1,120 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023-2024 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,
+)
+
+logger = logging.getLogger()
+
+
+class SetNameCommand(QUndoCommand):
+ def __init__(self, pollutants_lst, index, new_value):
+ QUndoCommand.__init__(self)
+
+ self._pollutants_lst = pollutants_lst
+ self._index = index
+ self._old = self._pollutants_lst.get(self._index).name
+ self._new = str(new_value)
+
+ def undo(self):
+ self._pollutants_lst.get(self._index).name = self._old
+
+ def redo(self):
+ self._pollutants_lst.get(self._index).name = self._new
+
+class SetEnabledCommand(QUndoCommand):
+ def __init__(self, pollutants_lst, index, enabled):
+ QUndoCommand.__init__(self)
+
+ self._pollutants_lst = pollutants_lst
+ self._index = index
+ self._old = not enabled
+ self._new = enabled
+
+ def undo(self):
+ self._pollutants_lst.get(self._index).enabled = self._old
+
+ def redo(self):
+ self._pollutants_lst.get(self._index).enabled = self._new
+
+
+class AddCommand(QUndoCommand):
+ def __init__(self, pollutants_lst, index):
+ QUndoCommand.__init__(self)
+
+ self._pollutants_lst = pollutants_lst
+
+ self._index = index
+ self._new = None
+
+ def undo(self):
+ self._pollutants_lst.delete_i([self._index])
+
+ def redo(self):
+ if self._new is None:
+ self._new = self._pollutants_lst.new(self._pollutants_lst, self._index)
+ else:
+ self._pollutants_lst.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+ def __init__(self, pollutants_lst, rows):
+ QUndoCommand.__init__(self)
+
+ self._pollutants_lst = pollutants_lst
+
+ self._rows = rows
+
+ self._pollutants = []
+ for row in rows:
+ self._pollutants.append((row, self._pollutants_lst.get(row)))
+ self._pollutants.sort()
+
+ def undo(self):
+ for row, el in self._pollutants:
+ self._pollutants_lst.insert(row, el)
+
+ def redo(self):
+ self._pollutants_lst.delete_i(self._rows)
+
+
+class PasteCommand(QUndoCommand):
+ def __init__(self, pollutants_lst, row, pollutant):
+ QUndoCommand.__init__(self)
+
+ self._pollutants_lst = pollutants_lst
+
+ self._row = row
+ self._pollutant = deepcopy(pollutant)
+ self._pollutant.reverse()
+
+ def undo(self):
+ self._pollutants_lst.delete_i(
+ self._tab,
+ range(self._row, self._row + len(self._pollutant))
+ )
+
+ def redo(self):
+ for r in self._pollutant:
+ self._pollutants_lst.insert(self._row, r)
diff --git a/src/View/Pollutants/Window.py b/src/View/Pollutants/Window.py
new file mode 100644
index 00000000..430061c9
--- /dev/null
+++ b/src/View/Pollutants/Window.py
@@ -0,0 +1,221 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023-2024 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 PyQt5 import QtCore
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel, QCoreApplication,
+ pyqtSlot, pyqtSignal, QItemSelectionModel,
+)
+
+from PyQt5.QtWidgets import (
+ QDialogButtonBox, QPushButton, QLineEdit,
+ QFileDialog, QTableView, QAbstractItemView,
+ QUndoStack, QShortcut, QAction, QItemDelegate,
+ QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.Pollutants.PlotAC import PlotAC
+from View.Pollutants.PlotKPC import PlotKPC
+
+from View.Pollutants.Table import (
+ TableModel
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.Pollutants.Translate import PollutantsTranslate
+
+from View.Pollutants.BasicHydraulicStructures.Window import (
+ BasicHydraulicStructuresWindow
+)
+
+logger = logging.getLogger()
+
+
+class PollutantsWindow(PamhyrWindow):
+ _pamhyr_ui = "Pollutants"
+ _pamhyr_name = "Pollutants"
+
+ def __init__(self, study=None, config=None, parent=None):
+ trad = PollutantsTranslate()
+ name = trad[self._pamhyr_name] + " - " + study.name
+
+ super(PollutantsWindow, self).__init__(
+ title=name,
+ study=study,
+ config=config,
+ trad=trad,
+ parent=parent
+ )
+
+ self._pollutants_lst = self._study._river._Pollutants
+
+ self.setup_table()
+ self.setup_checkbox()
+ self.setup_connections()
+
+ self.update()
+
+ def setup_table(self):
+ self._table = None
+
+ table = self.find(QTableView, f"tableView")
+ self._table = TableModel(
+ table_view=table,
+ table_headers=self._trad.get_dict("table_headers"),
+ editable_headers=["name"],
+ trad=self._trad,
+ data=self._study.river,
+ undo=self._undo_stack,
+ )
+
+ selectionModel = table.selectionModel()
+ index = table.model().index(0, 0)
+
+ selectionModel.select(
+ index,
+ QItemSelectionModel.Rows |
+ QItemSelectionModel.ClearAndSelect |
+ QItemSelectionModel.Select
+ )
+ table.scrollTo(index)
+
+ def setup_checkbox(self):
+ self._checkbox = self.find(QCheckBox, f"checkBox")
+ self._set_checkbox_state()
+
+ 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)
+ self._checkbox.clicked.connect(self._set_structure_state)
+
+ table = self.find(QTableView, "tableView")
+ table.selectionModel()\
+ .selectionChanged\
+ .connect(self.update)
+
+ self._table.dataChanged.connect(self.update)
+ self._table.layoutChanged.connect(self.update)
+
+ def index_selected(self):
+ table = self.find(QTableView, "tableView")
+ r = table.selectionModel().selectedRows()
+
+ if len(r) > 0:
+ return r[0]
+ else:
+ return None
+
+ def index_selected_row(self):
+ table = self.find(QTableView, "tableView")
+ r = table.selectionModel().selectedRows()
+
+ if len(r) > 0:
+ return r[0].row()
+ else:
+ return None
+
+ 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._pollutants_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._pollutants_lst.get(row)
+
+ if self.sub_window_exists(
+ BasicHydraulicStructuresWindow,
+ data=[self._study, None, data]
+ ):
+ continue
+
+ win = BasicHydraulicStructuresWindow(
+ data=data,
+ study=self._study,
+ parent=self
+ )
+ win.show()
+
+ def _set_checkbox_state(self):
+ row = self.index_selected_row()
+ if row is None:
+ self._checkbox.setEnabled(False)
+ self._checkbox.setChecked(True)
+ else:
+ self._checkbox.setEnabled(True)
+ self._checkbox.setChecked(self._pollutants_lst.get(row).enabled)
+
+ def _set_structure_state(self):
+ rows = self.index_selected_rows()
+ if len(rows) != 0:
+ for row in rows:
+ if row is not None:
+ self._table.enabled(
+ row,
+ self._checkbox.isChecked()
+ )
+
+ def update(self):
+ self._set_checkbox_state()
+
+
+
diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui
index dd5baf14..20ec6283 100644
--- a/src/View/ui/MainWindow.ui
+++ b/src/View/ui/MainWindow.ui
@@ -218,6 +218,7 @@
AdisTS
+
@@ -755,6 +756,11 @@
Run AdisTS
+
+
+ Pollutants
+
+
diff --git a/src/View/ui/Pollutants.ui b/src/View/ui/Pollutants.ui
new file mode 100644
index 00000000..213d583e
--- /dev/null
+++ b/src/View/ui/Pollutants.ui
@@ -0,0 +1,139 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 450
+
+
+
+ Hydraulic structures
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
-
+
+
+
+ 300
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ 5
+
+
-
+
+
+ Enable / Disable Pollutant
+
+
+ true
+
+
+
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+
+ ressources/add.pngressources/add.png
+
+
+ Add
+
+
+ Add a new point
+
+
+
+
+
+ ressources/del.pngressources/del.png
+
+
+ Delete
+
+
+ Delete points
+
+
+
+
+
+ ressources/edit.pngressources/edit.png
+
+
+ Edit
+
+
+ Edit selected hydraulic structure
+
+
+
+
+
+
diff --git a/tests_cases/Ardeche/Ardeche.pamhyr b/tests_cases/Ardeche/Ardeche.pamhyr
index 82a30096..d0c55c70 100644
Binary files a/tests_cases/Ardeche/Ardeche.pamhyr and b/tests_cases/Ardeche/Ardeche.pamhyr differ