diff --git a/src/Model/River.py b/src/Model/River.py
index 92d62146..7eeafa8d 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -66,6 +66,9 @@ from Model.InitialConditionsTemperature.InitialConditionsTemperatureList \
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList
+from Model.WeatherParameters.AirTemperatureList \
+ import AirTemperatureList
+
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Results.Results import Results
@@ -484,6 +487,7 @@ class River(Graph):
DIFAdisTSList,
InitialConditionsTemperatureList,
BoundaryConditionsTemperatureList,
+ AirTemperatureList,
GeoTIFFList,
Results
]
@@ -521,11 +525,13 @@ class River(Graph):
status=self._status)
self._D90AdisTS = D90AdisTSList(status=self._status)
self._DIFAdisTS = DIFAdisTSList(status=self._status)
- self._InitialConditionsTemperature = InitialConditionsTemperatureList(
- status=self._status)
+ self._InitialConditionsTemperature = (
+ InitialConditionsTemperatureList(status=self._status)
+ )
self._BoundaryConditionsTemperature = (
- BoundaryConditionsTemperatureList(
- status=self._status))
+ BoundaryConditionsTemperatureList(status=self._status)
+ )
+ self._AirTemperature = (AirTemperatureList(status=self._status))
self._geotiff = GeoTIFFList(status=self._status)
@@ -647,6 +653,8 @@ class River(Graph):
new._BoundaryConditionsTemperature = \
BoundaryConditionsTemperatureList._db_load(execute, data)
+ new._AirTemperature = AirTemperatureList._db_load(execute, data)
+
new._geotiff = GeoTIFFList._db_load(execute, data)
return new
@@ -684,6 +692,7 @@ class River(Graph):
objs.append(self._InitialConditionsTemperature)
objs.append(self._BoundaryConditionsTemperature)
+ objs.append(self._AirTemperature)
objs.append(self._geotiff)
@@ -765,6 +774,7 @@ class River(Graph):
self._D90AdisTS, self._DIFAdisTS,
self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature,
+ self._AirTemperature,
self._geotiff,
]
@@ -906,6 +916,10 @@ Last export at: @date."""
def boundary_conditions_temperature(self):
return self._BoundaryConditionsTemperature
+ @property
+ def air_temperature(self):
+ return self._AirTemperature
+
def get_params(self, solver):
if solver in self._parameters:
return self._parameters[solver]
diff --git a/src/Model/WeatherParameters/AirTemperature.py b/src/Model/WeatherParameters/AirTemperature.py
new file mode 100644
index 00000000..212ea52c
--- /dev/null
+++ b/src/Model/WeatherParameters/AirTemperature.py
@@ -0,0 +1,406 @@
+# AirTemperature.py -- Pamhyr
+# Copyright (C) 2023-2025 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
+from Model.Scenario import Scenario
+
+logger = logging.getLogger()
+
+
+class Data(SQLSubModel):
+ _sub_classes = []
+
+ def __init__(self,
+ data0, data1,
+ id: int = -1,
+ types=[float, float],
+ status=None,
+ owner_scenario=-1):
+ super(Data, self).__init__(
+ id=id, status=status,
+ owner_scenario=owner_scenario
+ )
+
+ self._types = types
+ self._data = [data0, data1]
+
+ @classmethod
+ def _db_create(cls, execute, ext=""):
+ execute(f"""
+ CREATE TABLE air_temperature_data{ext}(
+ {cls.create_db_add_pamhyr_id()},
+ deleted BOOLEAN NOT NULL DEFAULT FALSE,
+ data0 TEXT NOT NULL,
+ data1 TEXT NOT NULL,
+ {Scenario.create_db_add_scenario()},
+ {Scenario.create_db_add_scenario_fk()},
+ )
+ """)
+
+ @classmethod
+ def _db_update(cls, execute, version, data=None):
+ major, minor, release = version.strip().split(".")
+ created = False
+
+ if major == "0" and int(minor) < 2:
+ if cls.is_table_exists(
+ execute, "air_temperature_data"):
+ cls._db_update_to_0_2_0(execute, data)
+ else:
+ cls._db_create(execute)
+
+ return True
+
+ @classmethod
+ def _db_load(cls, execute, data=None):
+ new = []
+ status = data['status']
+ scenario = data["scenario"]
+ loaded = data['loaded_pid']
+
+ if scenario is None:
+ return new
+
+ table = execute(
+ "SELECT pamhyr_id, deleted, data0, data1, scenario " +
+ "FROM air_temperature_data " +
+ f"WHERE scenario = {scenario.id} " +
+ f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
+ )
+
+ if table is not None:
+ for row in table:
+ it = iter(row)
+
+ pid = next(it)
+ deleted = (next(it) == 1)
+ data0 = next(it)
+ data1 = next(it)
+ owner_scenario = next(it)
+
+ lc = cls(
+ data0, data1,
+ id=pid, status=status,
+ owner_scenario=owner_scenario
+ )
+ if deleted:
+ lc.set_as_deleted()
+
+ loaded.add(pid)
+ new.append(lc)
+
+ data["scenario"] = scenario.parent
+ new += cls._db_load(execute, data)
+ data["scenario"] = scenario
+
+ return new
+
+ def _db_save(self, execute, data=None):
+ if not self.must_be_saved():
+ return True
+
+ execute(
+ "INSERT INTO " +
+ "air_temperature_data(pamhyr_id, deleted," +
+ "data0, data1, scenario) " +
+ "VALUES (" +
+ f"{self.id}, {self._db_format(self.is_deleted())}, " +
+ f"{self._data[0]}, {self._data[1]}, " +
+ f"{self._status.scenario_id}" +
+ ")"
+ )
+
+ return True
+
+ def __getitem__(self, key):
+ return self._types[key](self._data[key])
+
+ def __setitem__(self, key, value):
+ self._data[key] = self._types[key](value)
+
+
+class AirTemperature(SQLSubModel):
+ _sub_classes = [Data]
+
+ def __init__(self, id: int = -1, int = -1,
+ name: str = "", status=None,
+ owner_scenario=-1):
+ super(AirTemperature, self).__init__(
+ id=id, status=status,
+ owner_scenario=owner_scenario
+ )
+
+ self._status = status
+
+ self._reach = None
+ self._begin_rk = 0.0
+ self._end_rk = 0.0
+ self._data = []
+ self._header = ["time", "rate"]
+ self._types = [self.time_convert, float]
+
+ @classmethod
+ def _db_create(cls, execute, ext=""):
+ execute(f"""
+ CREATE TABLE air_temperature{ext}(
+ {cls.create_db_add_pamhyr_id()},
+ deleted BOOLEAN NOT NULL DEFAULT FALSE,
+ reach INTEGER NOT NULL,
+ begin_rk REAL NOT NULL,
+ end_rk REAL NOT NULL,
+ {Scenario.create_db_add_scenario()},
+ {Scenario.create_db_add_scenario_fk()},
+ FOREIGN KEY(reach) REFERENCES river_reach(pamhyr_id)
+ )
+ """)
+
+ if ext != "":
+ return True
+
+ return cls._create_submodel(execute)
+
+ @classmethod
+ def _db_update(cls, execute, version, data=None):
+ major, minor, release = version.strip().split(".")
+ created = False
+
+ if major == "0" and int(minor) < 2:
+ if cls.is_table_exists(execute, "air_temperature"):
+ cls._db_update_to_0_2_0(execute, data)
+ else:
+ cls._db_create(execute)
+ created = True
+
+ if not created:
+ return cls._update_submodel(execute, version, data)
+
+ return True
+
+ @classmethod
+ def _db_load(cls, execute, data=None):
+ new = []
+ status = data['status']
+ scenario = data["scenario"]
+ loaded = data['loaded_pid']
+
+ if scenario is None:
+ return new
+
+ table = execute(
+ "SELECT pamhyr_id, deleted, reach, " +
+ "begin_rk, end_rk, scenario " +
+ "FROM air_temperature " +
+ f"WHERE scenario = {scenario.id} " +
+ f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) "
+ )
+
+ if table is not None:
+ for row in table:
+ it = iter(row)
+
+ pid = next(it)
+ deleted = (next(it) == 1)
+ reach = next(it)
+ brk = next(it)
+ erk = next(it)
+ owner_scenario = next(it)
+
+ data["scenario"] = scenario.parent
+ new += cls._db_load(execute, data)
+ data["scenario"] = scenario
+
+ return new
+
+ def _db_save(self, execute, data=None):
+ if not self.must_be_saved():
+ return True
+
+ execute(
+ f"DELETE FROM air_temperature " +
+ f"WHERE pamhyr_id = {self.id} " +
+ f"AND scenario = {self._status.scenario_id}"
+ )
+ execute(
+ f"DELETE FROM air_temperature_data " +
+ f"WHERE scenario = {self._status.scenario_id}"
+ )
+
+ execute(
+ "INSERT INTO " +
+ "air_temperature(pamhyr_id, deleted, " +
+ "reach, begin_rk, end_rk, scenario) " +
+ "VALUES (" +
+ f"{self.id}, {self._db_format(self.is_deleted())}, {self.reach}, " +
+ f"{self._begin_rk}, {self._end_rk}, " +
+ f"{self._status.scenario_id}" +
+ ")"
+ )
+ ind = 0
+ for d in self._data:
+ data["ind"] = ind
+
+ d._db_save(execute, data)
+
+ ind += 1
+
+ return True
+
+ def _data_traversal(self,
+ predicate=lambda obj, data: True,
+ modifier=lambda obj, data: None,
+ data={}):
+ if predicate(self, data):
+ modifier(self, data)
+
+ for d in self._data:
+ d._data_traversal(predicate, modifier, data)
+
+ def __len__(self):
+ return len(
+ list(
+ filter(
+ lambda el: el is not None and not el.is_deleted(),
+ self._data
+ )
+ )
+ )
+
+ @classmethod
+ def time_convert(cls, data):
+ if type(data) is str:
+ if data.count("-") == 2:
+ return date_iso_to_timestamp(data)
+ if data.count("/") == 2:
+ return date_dmy_to_timestamp(data)
+ if data.count(":") == 3:
+ return old_pamhyr_date_to_timestamp(data)
+ if data.count(":") == 2:
+ return old_pamhyr_date_to_timestamp("00:" + data)
+ if data.count(".") == 1:
+ return round(float(data))
+
+ return int(data)
+
+ @property
+ def reach(self):
+ return self._reach
+
+ @reach.setter
+ def reach(self, reach):
+ self._reach = reach
+ self.modified()
+
+ @property
+ def header(self):
+ return self._header.copy()
+
+ @header.setter
+ def header(self, header):
+ self._header = header
+ self.modified()
+
+ @property
+ def data(self):
+ return list(
+ filter(
+ lambda el: el is not None and not el.is_deleted(),
+ self._data
+ )
+ )
+
+ @property
+ def begin_rk(self):
+ return self._begin_rk
+
+ @begin_rk.setter
+ def begin_rk(self, begin_rk):
+ self._begin_rk = begin_rk
+ self.modified()
+
+ @property
+ def end_rk(self):
+ return self._end_rk
+
+ @end_rk.setter
+ def end_rk(self, end_rk):
+ self._end_rk = end_rk
+ self.modified()
+
+ @property
+ def _default_0(self):
+ return self._types[0](0)
+
+ @property
+ def _default_1(self):
+ return self._types[1](0.0)
+
+ def add(self, index: int):
+ value = Data(self._default_0, self._default_1, status=self._status)
+ self._data.insert(index, value)
+ self.modified()
+ return value
+
+ def insert(self, index: int, data: Data):
+ self._data.insert(index, data)
+ self.modified()
+
+ def delete_i(self, indexes):
+ self._data = list(
+ map(
+ lambda e: e[1].set_as_deleted(),
+ filter(
+ lambda e: e[0] not in indexes,
+ enumerate(self.data)
+ )
+ )
+ )
+ self.modified()
+
+ def index(self, bc):
+ self._data.index(bc)
+
+ def get_i(self, index: int):
+ 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: int, column: int, value):
+ v = self._data[index]
+ v[column] = self._types[column](value)
+ self._data[index] = v
+ self.modified()
+
+ def set_i_0(self, index: int, value):
+ self._set_i_c_v(index, 0, value)
+
+ def set_i_1(self, index: int, value):
+ self._set_i_c_v(index, 1, value)
diff --git a/src/Model/WeatherParameters/AirTemperatureList.py b/src/Model/WeatherParameters/AirTemperatureList.py
new file mode 100644
index 00000000..3875125d
--- /dev/null
+++ b/src/Model/WeatherParameters/AirTemperatureList.py
@@ -0,0 +1,73 @@
+# AirTemperatureList.py -- Pamhyr
+# Copyright (C) 2023-2025 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+from copy import copy
+from tools import trace, timer
+
+from Model.Tools.PamhyrListExt import PamhyrModelList
+from Model.Except import NotImplementedMethodeError
+
+from Model.WeatherParameters.AirTemperature \
+ import AirTemperature
+
+
+class AirTemperatureList(PamhyrModelList):
+ _sub_classes = [
+ AirTemperature,
+ ]
+
+ @classmethod
+ def _db_load(cls, execute, data=None):
+ new = cls(status=data['status'])
+
+ if data is None:
+ data = {}
+
+ new._lst = AirTemperature._db_load(
+ execute, data
+ )
+
+ return new
+
+ def _db_save(self, execute, data=None):
+ execute(
+ "DELETE FROM air_temperature " +
+ f"WHERE scenario = {self._status.scenario_id}"
+ )
+ execute(
+ "DELETE FROM air_temperature_data " +
+ f"WHERE scenario = {self._status.scenario_id}"
+ )
+
+ if data is None:
+ data = {}
+
+ for lc in self._lst:
+ lc._db_save(execute, data=data)
+
+ return True
+
+ def new(self, index, pollutant):
+ n = AirTemperature(pollutant=pollutant, status=self._status)
+ self._lst.insert(index, n)
+ self._status.modified()
+ return n
+
+ @property
+ def Air_Temperature_List(self):
+ return self.lst
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 7a25f529..104f5c50 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -114,6 +114,10 @@ from View.BoundaryConditionsTemperature.Window import (
BoundaryConditionsTemperatureWindow
)
+from View.WeatherParameters.Window import (
+ WeatherParametersWindow
+)
+
from Solver.Solvers import GenericSolver
from Model.Results.Results import Results
@@ -282,6 +286,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self.open_initial_conditions_temperature,
"action_menu_boundary_conditions_temperature":
self.open_boundary_conditions_temperature,
+ "action_menu_weather_parameters":
+ self.open_weather_parameters,
"action_menu_dif": self.open_dif,
"action_menu_d90": self.open_d90,
"action_menu_pollutants": self.open_pollutants,
@@ -1037,6 +1043,21 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# SUB WINDOWS #
###############
+ def open_weather_parameters(self):
+ river = self._study.river
+
+ if self.sub_window_exists(
+ WeatherParametersWindow,
+ data=[self._study, None]
+ ):
+ return
+
+ bound = WeatherParametersWindow(
+ study=self._study,
+ parent=self,
+ )
+ bound.show()
+
def open_boundary_conditions_temperature(self):
river = self._study.river
bclist = river.boundary_conditions_temperature.lst
diff --git a/src/View/WeatherParameters/Table.py b/src/View/WeatherParameters/Table.py
new file mode 100644
index 00000000..bc884ac7
--- /dev/null
+++ b/src/View/WeatherParameters/Table.py
@@ -0,0 +1,240 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023-2025 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.WeatherParameters.UndoCommand import (
+ SetCommand, AddCommand, SetCommandSpec,
+ DelCommand,
+)
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+ def __init__(self, data=None, _weather_param_lst=None,
+ trad=None, parent=None, mode="reaches"):
+ super(ComboBoxDelegate, self).__init__(parent)
+
+ self._data = data
+ self._mode = mode
+ self._trad = trad
+ # self._weather_param_lst = _weather_param_lst
+
+ def createEditor(self, parent, option, index):
+ self.editor = QComboBox(parent)
+
+ val = []
+ if self._mode == "rk":
+ reach_id = self._weather_param_lst[index.row()].reach
+
+ reach = next(filter(lambda edge: edge.id == reach_id,
+ self._data.edges()), None)
+
+ if reach_id is not None:
+ val = list(
+ map(
+ lambda rk: str(rk), reach.reach.get_rk()
+ )
+ )
+ else:
+ val = list(
+ map(
+ lambda n: n.name, self._data.edges()
+ )
+ )
+
+ self.editor.addItems(
+ [self._trad['not_associated']] +
+ val
+ )
+
+ self.editor.setCurrentText(str(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 WeatherParametersTableModel(PamhyrTableModel):
+ def __init__(self, river=None, data=None, **kwargs):
+ self._river = river
+
+ super(WeatherParametersTableModel, self).__init__(data=data, **kwargs)
+
+ self._data = data
+ print("self._data: ", self._data)
+
+ def _setup_lst(self):
+ # self._lst = list(
+ # filter(
+ # lambda ica: ica._deleted is False,
+ # self._data._data
+ # )
+ # )
+ self._lst = self._data._data
+
+ 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":
+ n = self._lst[row].name
+ if n is None or n == "":
+ return self._trad['not_associated']
+ return n
+ elif self._headers[column] == "reach":
+ n = self._lst[row].reach
+ if n is None:
+ return self._trad['not_associated']
+ return next(filter(
+ lambda edge: edge.id == n, self._river.edges()
+ )).name
+ elif self._headers[column] == "start_rk":
+ n = self._lst[row].start_rk
+ if n is None:
+ return self._trad['not_associated']
+ return n
+ elif self._headers[column] == "end_rk":
+ n = self._lst[row].end_rk
+ if n is None:
+ return self._trad['not_associated']
+ return n
+ elif self._headers[column] == "value":
+ n = self._lst[row].value
+ if n is None:
+ return self._trad['not_associated']
+ return n
+ return n
+
+ return QVariant()
+
+ def setData(self, index, value, role=Qt.EditRole):
+ if not index.isValid() or role != Qt.EditRole:
+ return False
+
+ if self.is_same_data(index, value):
+ return False
+
+ row = index.row()
+ column = index.column()
+
+ try:
+ if self._headers[column] != "reach":
+ self._undo.push(
+ SetCommandSpec(
+ self._lst, row, self._headers[column], value
+ )
+ )
+ elif self._headers[column] == "reach":
+ self._undo.push(
+ SetCommandSpec(
+ self._lst, row, self._headers[column],
+ self._river.edge(value).id
+ )
+ )
+ 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, self._lst, row
+ )
+ )
+
+ self._setup_lst()
+ self.endInsertRows()
+ self.layoutChanged.emit()
+
+ def delete(self, rows, parent=QModelIndex()):
+ self.beginRemoveRows(parent, rows[0], rows[-1])
+
+ data_rows = {
+ id(ica): i for i, ica in enumerate(self._data._data)
+ }
+ self._undo.push(
+ DelCommand(
+ self._data,
+ self._data._data,
+ [data_rows[id(self._lst[row])] for row in rows]
+ )
+ )
+
+ self._setup_lst()
+ self.endRemoveRows()
+ self.layoutChanged.emit()
+
+ def undo(self):
+ self._undo.undo()
+ self._setup_lst()
+ self.layoutChanged.emit()
+
+ def redo(self):
+ self._undo.redo()
+ self._setup_lst()
+ self.layoutChanged.emit()
diff --git a/src/View/WeatherParameters/TableDefault.py b/src/View/WeatherParameters/TableDefault.py
new file mode 100644
index 00000000..c95560d6
--- /dev/null
+++ b/src/View/WeatherParameters/TableDefault.py
@@ -0,0 +1,95 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023-2025 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.WeatherParameters.UndoCommand import (
+ SetCommand,
+)
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class WeatherParametersTableDefaultModel(PamhyrTableModel):
+ def __init__(self, **kwargs):
+ super(WeatherParametersTableDefaultModel, self).__init__(**kwargs)
+
+ 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[row]
+ elif self._headers[column] == "value":
+ n = 0.0
+ if n is None:
+ return self._trad['not_associated']
+ return n
+
+ 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] is not None:
+ self._undo.push(
+ SetCommand(
+ self._data, row, self._headers[column], value
+ )
+ )
+ except Exception as e:
+ logger.info(e)
+ logger.debug(traceback.format_exc())
+
+ self.dataChanged.emit(index, index)
+ return True
+
+ def undo(self):
+ self._undo.undo()
+ self.layoutChanged.emit()
+
+ def redo(self):
+ self._undo.redo()
+ self.layoutChanged.emit()
diff --git a/src/View/WeatherParameters/UndoCommand.py b/src/View/WeatherParameters/UndoCommand.py
new file mode 100644
index 00000000..984be6e7
--- /dev/null
+++ b/src/View/WeatherParameters/UndoCommand.py
@@ -0,0 +1,195 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023-2025 INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# -*- coding: utf-8 -*-
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+ QMessageBox, QUndoCommand, QUndoStack,
+)
+
+from Model.InitialConditionsAdisTS.InitialConditionsAdisTS \
+ import InitialConditionsAdisTS
+from Model.InitialConditionsAdisTS.InitialConditionsAdisTSList \
+ import InitialConditionsAdisTSList
+
+
+class SetCommand(QUndoCommand):
+ def __init__(self, data, row, column, new_value):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._row = row
+ self._column = column
+
+ if self._column == "name":
+ self._old = self._data[self._row].name
+ elif self._column == "concentration":
+ self._old = self._data[self._row].concentration
+ elif self._column == "eg":
+ self._old = self._data[self._row].eg
+ elif self._column == "em":
+ self._old = self._data[self._row].em
+ elif self._column == "ed":
+ self._old = self._data[self._row].ed
+
+ _type = float
+ if column == "name":
+ _type = str
+
+ self._new = _type(new_value)
+
+ def undo(self):
+ if self._column == "name":
+ self._data[self._row].name = self._old
+ elif self._column == "concentration":
+ self._data[self._row].concentration = self._old
+ elif self._column == "eg":
+ self._data[self._row].eg = self._old
+ elif self._column == "em":
+ self._data[self._row].em = self._old
+ elif self._column == "ed":
+ self._data[self._row].ed = self._old
+
+ def redo(self):
+ if self._column == "name":
+ self._data[self._row].name = self._new
+ elif self._column == "concentration":
+ self._data[self._row].concentration = self._new
+ elif self._column == "eg":
+ self._data[self._row].eg = self._new
+ elif self._column == "em":
+ self._data[self._row].em = self._new
+ elif self._column == "ed":
+ self._data[self._row].ed = self._new
+
+
+class SetCommandSpec(QUndoCommand):
+ def __init__(self, data, row, column, new_value):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._row = row
+ self._column = column
+
+ if self._column == "name":
+ self._old = self._data[self._row].name
+ elif self._column == "reach":
+ self._old = self._data[self._row].reach
+ elif self._column == "start_rk":
+ self._old = self._data[self._row].start_rk
+ elif self._column == "end_rk":
+ self._old = self._data[self._row].end_rk
+ elif self._column == "concentration":
+ self._old = self._data[self._row].concentration
+ elif self._column == "eg":
+ self._old = self._data[self._row].eg
+ elif self._column == "em":
+ self._old = self._data[self._row].em
+ elif self._column == "ed":
+ self._old = self._data[self._row].ed
+ elif self._column == "rate":
+ self._old = self._data[self._row].rate
+
+ _type = float
+ if column == "name":
+ _type = str
+ elif column == "reach":
+ _type = int
+
+ self._new = _type(new_value)
+
+ def undo(self):
+ if self._column == "name":
+ self._data[self._row].name = self._old
+ elif self._column == "reach":
+ self._data[self._row].reach = self._old
+ elif self._column == "start_rk":
+ self._data[self._row].start_rk = self._old
+ elif self._column == "end_rk":
+ self._data[self._row].end_rk = self._old
+ elif self._column == "concentration":
+ self._data[self._row].concentration = self._old
+ elif self._column == "eg":
+ self._data[self._row].eg = self._old
+ elif self._column == "em":
+ self._data[self._row].em = self._old
+ elif self._column == "ed":
+ self._data[self._row].ed = self._old
+ elif self._column == "rate":
+ self._data[self._row].rate = self._old
+
+ def redo(self):
+ if self._column == "name":
+ self._data[self._row].name = self._new
+ elif self._column == "reach":
+ self._data[self._row].reach = self._new
+ elif self._column == "start_rk":
+ self._data[self._row].start_rk = self._new
+ elif self._column == "end_rk":
+ self._data[self._row].end_rk = self._new
+ elif self._column == "concentration":
+ self._data[self._row].concentration = self._new
+ elif self._column == "eg":
+ self._data[self._row].eg = self._new
+ elif self._column == "em":
+ self._data[self._row].em = self._new
+ elif self._column == "ed":
+ self._data[self._row].ed = self._new
+ elif self._column == "rate":
+ self._data[self._row].rate = self._new
+
+
+class AddCommand(QUndoCommand):
+ def __init__(self, data, ics_spec, index):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._ics_spec = ics_spec
+ 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.new(self._index)
+ else:
+ self._data.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+ def __init__(self, data, ics_spec, rows):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._ics_spec = ics_spec
+ self._rows = rows
+
+ self._ic = []
+ for row in rows:
+ self._ic.append((row, self._ics_spec[row]))
+ self._ic.sort()
+
+ def undo(self):
+ for row, el in self._ic:
+ self._data.insert(row, el)
+
+ def redo(self):
+ self._data.delete_i(self._rows)
diff --git a/src/View/WeatherParameters/Window.py b/src/View/WeatherParameters/Window.py
new file mode 100644
index 00000000..4aa0c7ab
--- /dev/null
+++ b/src/View/WeatherParameters/Window.py
@@ -0,0 +1,313 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023-2025 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 os
+import logging
+
+from tools import trace, timer, logger_exception
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+
+from PyQt5.QtGui import (
+ QKeySequence, QIcon,
+)
+
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel,
+ QCoreApplication, QModelIndex, pyqtSlot,
+ QRect, QItemSelectionModel,
+)
+
+from PyQt5.QtWidgets import (
+ QDialogButtonBox, QPushButton, QLineEdit,
+ QFileDialog, QTableView, QAbstractItemView,
+ QUndoStack, QShortcut, QAction, QItemDelegate,
+ QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
+ QVBoxLayout, QToolBar, QAction, QToolButton,
+)
+
+from Modules import Modules
+
+from View.WeatherParameters.UndoCommand import (
+ SetCommand,
+)
+
+from View.WeatherParameters.TableDefault import (
+ WeatherParametersTableDefaultModel,
+)
+
+from View.WeatherParameters.Table import (
+ WeatherParametersTableModel, ComboBoxDelegate,
+)
+
+from View.WeatherParameters.translate import WeatherParametersTranslate
+
+from Solver.Mage import Mage8
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class WeatherParametersWindow(PamhyrWindow):
+ _pamhyr_ui = "WeatherParameters"
+ _pamhyr_name = "Weather parameters"
+
+
+ def __init__(self, data=None, study=None,
+ config=None, parent=None):
+ self._data = []
+ # self._data.append(data)
+ trad = WeatherParametersTranslate()
+
+ self.weather_params = [trad.get_dict("weather_parameters")[k] for k in trad.get_dict("weather_parameters").keys()]
+ print(f"Weather parameters: {self.weather_params}")
+ name = (
+ trad[self._pamhyr_name] +
+ " - " + study.name
+ )
+
+ super(WeatherParametersWindow, self).__init__(
+ title=name,
+ study=study,
+ config=config,
+ trad=trad,
+ parent=parent
+ )
+
+ self._hash_data.append(data)
+
+ # self._ics_adists_lst = study.river.ic_adists
+
+ self.setup_table()
+
+ def setup_table(self):
+ path_icons = os.path.join(self._get_ui_directory(), f"ressources")
+
+ table_default = self.find(QTableView, f"tableView")
+
+ if self._study.is_editable():
+ editable_headers = self._trad.get_dict("table_headers")
+ else:
+ editable_headers = []
+
+ self._table = WeatherParametersTableDefaultModel(
+ table_view=table_default,
+ table_headers=self._trad.get_dict("table_headers"),
+ editable_headers=editable_headers,
+ delegates={},
+ data=self.weather_params,
+ undo=self._undo_stack,
+ trad=self._trad
+ )
+
+ table_default.setModel(self._table)
+ table_default.setSelectionBehavior(QAbstractItemView.SelectRows)
+ table_default.horizontalHeader().setSectionResizeMode(
+ QHeaderView.Stretch
+ )
+ table_default.setAlternatingRowColors(True)
+
+ layout = self.find(QVBoxLayout, f"verticalLayout_1")
+ toolBar = QToolBar()
+ layout.addWidget(toolBar)
+
+ action_add = QAction(self)
+ action_add.setIcon(QIcon(os.path.join(path_icons, f"add.png")))
+
+ if self._study.is_editable():
+ action_add.triggered.connect(self.add)
+ action_delete = QAction(self)
+ action_delete.setIcon(QIcon(os.path.join(path_icons, f"del.png")))
+
+ if self._study.is_editable():
+ action_delete.triggered.connect(self.delete)
+
+ toolBar.addAction(action_add)
+ toolBar.addAction(action_delete)
+
+ self.table_spec = QTableView()
+ layout.addWidget(self.table_spec)
+
+ self._delegate_reach = ComboBoxDelegate(
+ trad=self._trad,
+ data=self._study.river,
+ parent=self,
+ mode="reaches"
+ )
+ self._delegate_rk = ComboBoxDelegate(
+ trad=self._trad,
+ data=self._study.river,
+ parent=self,
+ mode="rk"
+ )
+
+ if self._study.is_editable():
+ editable_headers_spec = self._trad.get_dict("table_headers_spec")
+ else:
+ editable_headers_spec = []
+
+ self._table_spec = WeatherParametersTableModel(
+ table_view=self.table_spec,
+ table_headers=self._trad.get_dict("table_headers_spec"),
+ editable_headers=editable_headers_spec,
+ delegates={
+ "reach": self._delegate_reach,
+ "start_rk": self._delegate_rk,
+ "end_rk": self._delegate_rk
+ },
+ data=self._data,
+ undo=self._undo_stack,
+ trad=self._trad,
+ river=self._study.river
+ )
+
+ self.table_spec.setModel(self._table_spec)
+ self.table_spec.setSelectionBehavior(QAbstractItemView.SelectRows)
+ self.table_spec.horizontalHeader().setSectionResizeMode(
+ QHeaderView.Stretch
+ )
+ self.table_spec.setAlternatingRowColors(True)
+
+ selectionModel = self.table_spec.selectionModel()
+ index = self.table_spec.model().index(0, 0)
+
+ selectionModel.select(
+ index,
+ QItemSelectionModel.Rows |
+ QItemSelectionModel.ClearAndSelect |
+ QItemSelectionModel.Select
+ )
+ self.table_spec.scrollTo(index)
+
+ def index_selected_row(self):
+ # table = self.find(QTableView, f"tableView")
+ table = self.table_spec
+ rows = table.selectionModel()\
+ .selectedRows()
+
+ if len(rows) == 0:
+ return 0
+
+ return rows[0].row()
+
+ def index_selected_rows(self):
+ # table = self.find(QTableView, f"tableView")
+ table = self.table_spec
+ return list(
+ # Delete duplicate
+ set(
+ map(
+ lambda i: i.row(),
+ table.selectedIndexes()
+ )
+ )
+ )
+
+ def move_up(self):
+ row = self.index_selected_row()
+ self._table.move_up(row)
+ self._update()
+
+ def move_down(self):
+ row = self.index_selected_row()
+ self._table.move_down(row)
+ self._update()
+
+ def _copy(self):
+ rows = list(
+ map(
+ lambda row: row.row(),
+ self.tableView.selectionModel().selectedRows()
+ )
+ )
+
+ table = list(
+ map(
+ lambda eic: list(
+ map(
+ lambda k: eic[1][k],
+ ["rk", "discharge", "elevation"]
+ )
+ ),
+ filter(
+ lambda eic: eic[0] in rows,
+ enumerate(self._ics.lst())
+ )
+ )
+ )
+
+ self.copyTableIntoClipboard(table)
+
+ def _paste(self):
+ header, data = self.parseClipboardTable()
+
+ if len(data) + len(header) == 0:
+ return
+
+ logger.debug(
+ "IC: Paste: " +
+ f"header = {header}, " +
+ f"data = {data}"
+ )
+
+ try:
+ row = self.index_selected_row()
+ # self._table.paste(row, header, data)
+ self._table.paste(row, [], data)
+ except Exception as e:
+ logger_exception(e)
+
+ self._update()
+
+ def _undo(self):
+ undo_stack = self._undo_stack
+ if undo_stack is None or not undo_stack.canUndo():
+ return
+
+ if isinstance(undo_stack.command(undo_stack.index() - 1), SetCommand):
+ self._table.undo()
+ else:
+ self._table_spec.undo()
+
+ self._update()
+
+ def _redo(self):
+ undo_stack = self._undo_stack
+ if undo_stack is None or not undo_stack.canRedo():
+ return
+
+ if isinstance(undo_stack.command(undo_stack.index()), SetCommand):
+ self._table.redo()
+ else:
+ self._table_spec.redo()
+
+ self._update()
+
+ def add(self):
+ rows = self.index_selected_rows()
+ if len(self._data[0]._data) == 0 or len(rows) == 0:
+ self._table_spec.add(0)
+ else:
+ self._table_spec.add(rows[0])
+
+ def delete(self):
+ rows = self.index_selected_rows()
+ if len(rows) == 0:
+ return
+ self._table_spec.delete(rows)
diff --git a/src/View/WeatherParameters/translate.py b/src/View/WeatherParameters/translate.py
new file mode 100644
index 00000000..4da9de4f
--- /dev/null
+++ b/src/View/WeatherParameters/translate.py
@@ -0,0 +1,50 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023-2025 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 WeatherParametersTranslate(MainTranslate):
+ def __init__(self):
+ super(WeatherParametersTranslate, self).__init__()
+
+ self._dict["Weather parameters"] = _translate(
+ "WeatherParameters", "Weather parameters")
+
+ self._dict["rk"] = self._dict["unit_rk"]
+
+ self._sub_dict["table_headers"] = {
+ "name": self._dict["name"],
+ "value": self._dict["value"]
+ }
+
+ self._sub_dict["table_headers_spec"] = {
+ "name": self._dict["name"],
+ "reach": self._dict["reach"],
+ "start_rk": _translate("Unit", "Start_RK (m)"),
+ "end_rk": _translate("Unit", "End_RK (m)"),
+ "value": self._dict["value"]
+ }
+
+ self._sub_dict["weather_parameters"] = {
+ "air_temperature": _translate("WeatherParameters", "Air temperature")
+ }
\ No newline at end of file
diff --git a/src/View/ui/WeatherParameters.ui b/src/View/ui/WeatherParameters.ui
new file mode 100644
index 00000000..b4fe3e2d
--- /dev/null
+++ b/src/View/ui/WeatherParameters.ui
@@ -0,0 +1,73 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 1024
+ 576
+
+
+
+ MainWindow
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
-
+
+
+ -
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+