WeatherParameters: Add first parameters view + model

monopk-ic-adistt
JeaDylan 2026-06-30 08:49:35 +02:00
parent 2cbfaec0c2
commit b2ca1c2ab7
10 changed files with 1484 additions and 4 deletions

View File

@ -66,6 +66,9 @@ from Model.InitialConditionsTemperature.InitialConditionsTemperatureList \
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \ from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList import BoundaryConditionsTemperatureList
from Model.WeatherParameters.AirTemperatureList \
import AirTemperatureList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Results.Results import Results from Model.Results.Results import Results
@ -484,6 +487,7 @@ class River(Graph):
DIFAdisTSList, DIFAdisTSList,
InitialConditionsTemperatureList, InitialConditionsTemperatureList,
BoundaryConditionsTemperatureList, BoundaryConditionsTemperatureList,
AirTemperatureList,
GeoTIFFList, GeoTIFFList,
Results Results
] ]
@ -521,11 +525,13 @@ class River(Graph):
status=self._status) status=self._status)
self._D90AdisTS = D90AdisTSList(status=self._status) self._D90AdisTS = D90AdisTSList(status=self._status)
self._DIFAdisTS = DIFAdisTSList(status=self._status) self._DIFAdisTS = DIFAdisTSList(status=self._status)
self._InitialConditionsTemperature = InitialConditionsTemperatureList( self._InitialConditionsTemperature = (
status=self._status) InitialConditionsTemperatureList(status=self._status)
)
self._BoundaryConditionsTemperature = ( self._BoundaryConditionsTemperature = (
BoundaryConditionsTemperatureList( BoundaryConditionsTemperatureList(status=self._status)
status=self._status)) )
self._AirTemperature = (AirTemperatureList(status=self._status))
self._geotiff = GeoTIFFList(status=self._status) self._geotiff = GeoTIFFList(status=self._status)
@ -647,6 +653,8 @@ class River(Graph):
new._BoundaryConditionsTemperature = \ new._BoundaryConditionsTemperature = \
BoundaryConditionsTemperatureList._db_load(execute, data) BoundaryConditionsTemperatureList._db_load(execute, data)
new._AirTemperature = AirTemperatureList._db_load(execute, data)
new._geotiff = GeoTIFFList._db_load(execute, data) new._geotiff = GeoTIFFList._db_load(execute, data)
return new return new
@ -684,6 +692,7 @@ class River(Graph):
objs.append(self._InitialConditionsTemperature) objs.append(self._InitialConditionsTemperature)
objs.append(self._BoundaryConditionsTemperature) objs.append(self._BoundaryConditionsTemperature)
objs.append(self._AirTemperature)
objs.append(self._geotiff) objs.append(self._geotiff)
@ -765,6 +774,7 @@ class River(Graph):
self._D90AdisTS, self._DIFAdisTS, self._D90AdisTS, self._DIFAdisTS,
self._InitialConditionsTemperature, self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature, self._BoundaryConditionsTemperature,
self._AirTemperature,
self._geotiff, self._geotiff,
] ]
@ -906,6 +916,10 @@ Last export at: @date."""
def boundary_conditions_temperature(self): def boundary_conditions_temperature(self):
return self._BoundaryConditionsTemperature return self._BoundaryConditionsTemperature
@property
def air_temperature(self):
return self._AirTemperature
def get_params(self, solver): def get_params(self, solver):
if solver in self._parameters: if solver in self._parameters:
return self._parameters[solver] return self._parameters[solver]

View File

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

View File

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

View File

@ -114,6 +114,10 @@ from View.BoundaryConditionsTemperature.Window import (
BoundaryConditionsTemperatureWindow BoundaryConditionsTemperatureWindow
) )
from View.WeatherParameters.Window import (
WeatherParametersWindow
)
from Solver.Solvers import GenericSolver from Solver.Solvers import GenericSolver
from Model.Results.Results import Results from Model.Results.Results import Results
@ -282,6 +286,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self.open_initial_conditions_temperature, self.open_initial_conditions_temperature,
"action_menu_boundary_conditions_temperature": "action_menu_boundary_conditions_temperature":
self.open_boundary_conditions_temperature, self.open_boundary_conditions_temperature,
"action_menu_weather_parameters":
self.open_weather_parameters,
"action_menu_dif": self.open_dif, "action_menu_dif": self.open_dif,
"action_menu_d90": self.open_d90, "action_menu_d90": self.open_d90,
"action_menu_pollutants": self.open_pollutants, "action_menu_pollutants": self.open_pollutants,
@ -1037,6 +1043,21 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# SUB WINDOWS # # 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): def open_boundary_conditions_temperature(self):
river = self._study.river river = self._study.river
bclist = river.boundary_conditions_temperature.lst bclist = river.boundary_conditions_temperature.lst

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,73 @@
<?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>1024</width>
<height>576</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_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout"/>
</item>
<item>
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_1"/>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_2"/>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1024</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>
</widget>
</widget>
<resources/>
<connections/>
</ui>