diff --git a/src/Model/BoundaryConditionsTemperature/BoundaryConditionTemperature.py b/src/Model/BoundaryConditionsTemperature/BoundaryConditionTemperature.py
new file mode 100644
index 00000000..c5b773f4
--- /dev/null
+++ b/src/Model/BoundaryConditionsTemperature/BoundaryConditionTemperature.py
@@ -0,0 +1,478 @@
+# BoundaryConditionsTemperature.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 boundary_condition_data_temperature{ext}(
+ {cls.create_db_add_pamhyr_id()},
+ deleted BOOLEAN NOT NULL DEFAULT FALSE,
+ data0 TEXT NOT NULL,
+ data1 TEXT NOT NULL,
+ bca INTEGER,
+ {Scenario.create_db_add_scenario()},
+ {Scenario.create_db_add_scenario_fk()},
+ FOREIGN KEY(bca)
+ REFERENCES boundary_condition_temperature(pamhyr_id)
+ )
+ """)
+
+ return True
+
+ @classmethod
+ def _db_update(cls, execute, version, data=None):
+ major, minor, release = version.strip().split(".")
+ created = False
+ if major == "0" and minor == "0":
+ if int(release) < 11:
+ cls._db_create(execute)
+ created = True
+
+ if major == "0" and (int(minor) < 2 or
+ (int(minor) == 2 and int(release) < 7)):
+ if not cls.is_table_exists(execute,
+ "boundary_condition_data_temperature"):
+ 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
+
+ bca = data["bca"]
+
+ table = execute(
+ "SELECT pamhyr_id, deleted, data0, data1, scenario FROM " +
+ "boundary_condition_data_temperature " +
+ f"WHERE scenario = {scenario.id} " +
+ # f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
+ f"AND bca = {bca.id}"
+ )
+
+ 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)
+
+ bc = cls(
+ data0, data1,
+ id=pid,
+ status=status,
+ owner_scenario=owner_scenario
+ )
+ if deleted:
+ bc.set_as_deleted()
+
+ loaded.add(pid)
+ new.append(bc)
+
+ 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
+
+ bca = data["bca"]
+
+ data0 = self._db_format(self._data[0])
+ data1 = self._db_format(self._data[1])
+
+ execute(
+ "INSERT INTO " +
+ "boundary_condition_data_temperature(" +
+ "pamhyr_id, deleted, data0, data1, bca, scenario) " +
+ f"VALUES ({self.id}, {self._db_format(self.is_deleted())}, " +
+ f"'{data0}', {data1}, {bca.id}, " +
+ 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 BoundaryConditionTemperature(SQLSubModel):
+ _sub_classes = [Data]
+
+ def __init__(self, id: int = -1, status=None, owner_scenario=-1):
+ super(BoundaryConditionTemperature, self).__init__(
+ id=id, status=status,
+ owner_scenario=owner_scenario
+ )
+
+ self._status = status
+
+ self._name = ""
+ self._node = None
+ self._data = []
+ self._header = []
+ self._types = [self.time_convert, float]
+
+ @classmethod
+ def _db_create(cls, execute, ext=""):
+ execute(f"""
+ CREATE TABLE boundary_condition_temperature{ext}(
+ {cls.create_db_add_pamhyr_id()},
+ deleted BOOLEAN NOT NULL DEFAULT FALSE,
+ name TEXT NOT NULL,
+ node INTEGER,
+ {Scenario.create_db_add_scenario()},
+ {Scenario.create_db_add_scenario_fk()},
+ FOREIGN KEY(node) REFERENCES river_node(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 or (
+ int(minor) == 2 and int(release) < 7)):
+ if not cls.is_table_exists(execute,
+ "boundary_condition_temperature"):
+ 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']
+ nodes = data['nodes']
+ scenario = data["scenario"]
+ loaded = data['loaded_pid']
+
+ if scenario is None:
+ return new
+
+ sql = (
+ "SELECT pamhyr_id, deleted, name, node, scenario "
+ "FROM boundary_condition_temperature " +
+ f"WHERE scenario = {scenario.id} "
+ )
+
+ table = execute(sql)
+
+ if table is not None:
+ for row in table:
+ it = iter(row)
+
+ pid = next(it)
+ deleted = (next(it) == 1)
+ bc_name = next(it)
+ node = next(it)
+ owner_scenario = next(it)
+
+ bc = cls(
+ id=pid,
+ status=status,
+ owner_scenario=owner_scenario
+ )
+ if deleted:
+ bc.set_as_deleted()
+
+ bc.name = bc_name
+
+ bc.node = None
+ if node != -1:
+ tmp = next(
+ filter(
+ lambda n: n.id == node, nodes
+ ),
+ None
+ )
+ if tmp is not None:
+ bc.node = tmp.id
+ else:
+ bc.node = -1
+
+ data["bca"] = bc
+ bc._data = Data._db_load(execute, data=data)
+
+ loaded.add(pid)
+ new.append(bc)
+
+ 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(
+ "DELETE FROM boundary_condition_temperature " +
+ f"WHERE pamhyr_id = {self.id} " +
+ f"AND scenario = {self._status.scenario_id} "
+ )
+ execute(
+ "DELETE FROM boundary_condition_data_temperature " +
+ f"WHERE bca = {self.id} " +
+ f"AND scenario = {self._status.scenario_id} "
+ )
+
+ node = -1
+ if self._node is not None:
+ node = self._node
+
+ execute(
+ "INSERT INTO " +
+ "boundary_condition_temperature(" +
+ "pamhyr_id, deleted, name, " +
+ "node, scenario) " +
+ "VALUES (" +
+ f"{self.id}, {self._db_format(self.is_deleted())}, " +
+ f"'{self._db_format(self._name)}', {node}, " +
+ f"{self._status.scenario_id}" +
+ ")"
+ )
+
+ data["bca"] = self
+
+ 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 name(self):
+ return self._name
+
+ @name.setter
+ def name(self, name):
+ self._name = name
+ self.modified()
+
+ @property
+ def node(self):
+ return self._node
+
+ @node.setter
+ def node(self, node):
+ self._node = node
+ 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 _default_0(self):
+ return self._types[0](0)
+
+ @property
+ def _default_1(self):
+ return self._types[1](0.0)
+
+ def new_from_data(self, header, data):
+ new_0 = self._default_0
+ new_1 = self._default_1
+
+ if len(header) != 0:
+ for i in [0, 1]:
+ for j in range(len(header)):
+ if self._header[i] == header[j]:
+ if i == 0:
+ new_0 = self._types[i](data[j].replace(",", "."))
+ else:
+ new_1 = self._types[i](data[j].replace(",", "."))
+ else:
+ new_0 = self._types[0](data[0].replace(",", "."))
+ new_1 = self._types[1](data[1].replace(",", "."))
+
+ return (new_0, new_1)
+
+ 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, value):
+ self._data.insert(index, value)
+ self.modified()
+
+ def delete_i(self, indexes):
+ list(
+ map(
+ lambda e: e[1].set_as_deleted(),
+ filter(
+ lambda e: e[0] in indexes,
+ enumerate(self.data)
+ )
+ )
+ )
+ self.modified()
+
+ def sort(self, _reverse=False, key=None):
+ if key is None:
+ self._data.sort(reverse=_reverse)
+ else:
+ self._data.sort(reverse=_reverse, key=key)
+ self.modified()
+
+ def index(self, bc):
+ self._data.index(bc)
+
+ def get_i(self, index):
+ return self.data[index]
+
+ def get_range(self, _range):
+ lst = []
+ for r in _range:
+ lst.append(r)
+ return lst
+
+ def _set_i_c_v(self, index, column, value):
+ v = 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/BoundaryConditionsTemperature/BoundaryConditionsTemperatureList.py b/src/Model/BoundaryConditionsTemperature/BoundaryConditionsTemperatureList.py
new file mode 100644
index 00000000..1f1fa3c4
--- /dev/null
+++ b/src/Model/BoundaryConditionsTemperature/BoundaryConditionsTemperatureList.py
@@ -0,0 +1,71 @@
+# BoundaryConditionsTemperatureList.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.BoundaryConditionsTemperature.BoundaryConditionTemperature \
+ import BoundaryConditionTemperature
+
+
+class BoundaryConditionsTemperatureList(PamhyrModelList):
+ _sub_classes = [
+ BoundaryConditionTemperature,
+ ]
+
+ @classmethod
+ def _db_load(cls, execute, data=None):
+ new = cls(status=data['status'])
+
+ if data is None:
+ data = {}
+
+ new._lst = BoundaryConditionTemperature._db_load(execute, data)
+
+ return new
+
+ def _db_save(self, execute, data=None):
+ execute(
+ "DELETE FROM boundary_condition_temperature " +
+ f"WHERE scenario = {self._status.scenario_id}"
+ )
+ execute(
+ "DELETE FROM boundary_condition_data_temperature " +
+ f"WHERE scenario = {self._status.scenario_id}"
+ )
+
+ if data is None:
+ data = {}
+
+ for bca in self._lst:
+ bca._db_save(execute, data=data)
+
+ return True
+
+ def new(self, index):
+ n = BoundaryConditionTemperature(status=self._status)
+ self._lst.insert(index, n)
+ self._status.modified()
+ return n
+
+ @property
+ def BCs_Temperature_List(self):
+ return self.lst
diff --git a/src/Model/River.py b/src/Model/River.py
index eb53957f..92d62146 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -63,6 +63,9 @@ from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.InitialConditionsTemperature.InitialConditionsTemperatureList \
import InitialConditionsTemperatureList
+from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
+ import BoundaryConditionsTemperatureList
+
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Results.Results import Results
@@ -480,6 +483,7 @@ class River(Graph):
D90AdisTSList,
DIFAdisTSList,
InitialConditionsTemperatureList,
+ BoundaryConditionsTemperatureList,
GeoTIFFList,
Results
]
@@ -519,6 +523,9 @@ class River(Graph):
self._DIFAdisTS = DIFAdisTSList(status=self._status)
self._InitialConditionsTemperature = InitialConditionsTemperatureList(
status=self._status)
+ self._BoundaryConditionsTemperature = (
+ BoundaryConditionsTemperatureList(
+ status=self._status))
self._geotiff = GeoTIFFList(status=self._status)
@@ -637,6 +644,9 @@ class River(Graph):
new._InitialConditionsTemperature = \
InitialConditionsTemperatureList._db_load(execute, data)
+ new._BoundaryConditionsTemperature = \
+ BoundaryConditionsTemperatureList._db_load(execute, data)
+
new._geotiff = GeoTIFFList._db_load(execute, data)
return new
@@ -671,7 +681,9 @@ class River(Graph):
objs.append(self._LateralContributionsAdisTS)
objs.append(self._D90AdisTS)
objs.append(self._DIFAdisTS)
+
objs.append(self._InitialConditionsTemperature)
+ objs.append(self._BoundaryConditionsTemperature)
objs.append(self._geotiff)
@@ -752,6 +764,7 @@ class River(Graph):
self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS,
self._InitialConditionsTemperature,
+ self._BoundaryConditionsTemperature,
self._geotiff,
]
@@ -889,6 +902,10 @@ Last export at: @date."""
def ic_temperature(self):
return self._InitialConditionsTemperature
+ @property
+ def boundary_conditions_temperature(self):
+ return self._BoundaryConditionsTemperature
+
def get_params(self, solver):
if solver in self._parameters:
return self._parameters[solver]
diff --git a/src/View/BoundaryConditionsTemperature/Edit/Plot.py b/src/View/BoundaryConditionsTemperature/Edit/Plot.py
new file mode 100644
index 00000000..b320be27
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/Edit/Plot.py
@@ -0,0 +1,105 @@
+# Plot.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 datetime import datetime
+
+from tools import timer, trace
+from View.Tools.PamhyrPlot import PamhyrPlot
+
+from PyQt5.QtCore import (
+ QCoreApplication
+)
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class Plot(PamhyrPlot):
+ def __init__(self, mode="time", data=None,
+ trad=None, canvas=None, toolbar=None,
+ parent=None):
+ super(Plot, self).__init__(
+ canvas=canvas,
+ trad=trad,
+ data=data,
+ toolbar=toolbar,
+ parent=parent
+ )
+
+ self._table_headers = self._trad.get_dict("table_headers")
+
+ header = self.data.header
+
+ self.label_x = self._table_headers[header[0]]
+ self.label_y = self._table_headers[header[1]]
+
+ self._mode = mode
+ self._isometric_axis = False
+
+ self._auto_relim_update = True
+ self._autoscale_update = False
+
+ def custom_ticks(self):
+ if self.data.header[0] != "time":
+ return
+
+ self.set_ticks_time_formater()
+
+ @timer
+ def draw(self):
+ self.init_axes()
+
+ if len(self.data) == 0:
+ self._init = False
+ return
+
+ self.draw_data()
+ self.custom_ticks()
+
+ self.idle()
+ self._init = True
+
+ def draw_data(self):
+ # Plot data
+ x = list(map(lambda v: v[0], self.data.data))
+ y = list(map(lambda v: v[1], self.data.data))
+
+ self._line, = self.canvas.axes.plot(
+ x, y,
+ color=self.color_plot,
+ **self.plot_default_kargs
+ )
+
+ @timer
+ def update(self, ind=None):
+ if not self._init:
+ self.draw()
+ return
+
+ self.update_data()
+
+ self.update_idle()
+
+ def update_data(self):
+ x = list(map(lambda v: v[0], self.data.data))
+ y = list(map(lambda v: v[1], self.data.data))
+
+ self._line.set_data(x, y)
diff --git a/src/View/BoundaryConditionsTemperature/Edit/Table.py b/src/View/BoundaryConditionsTemperature/Edit/Table.py
new file mode 100644
index 00000000..812cc5ec
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/Edit/Table.py
@@ -0,0 +1,185 @@
+# 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 datetime import date, time, datetime, timedelta
+
+from tools import (
+ trace, timer,
+ timestamp_to_old_pamhyr_date,
+ old_pamhyr_date_to_timestamp
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel,
+ QCoreApplication, QModelIndex, pyqtSlot,
+ QRect, QTime, QDateTime,
+)
+
+from PyQt5.QtWidgets import (
+ QTableView, QAbstractItemView, QSpinBox,
+ QTimeEdit, QDateTimeEdit, QItemDelegate,
+)
+
+from View.BoundaryConditionsTemperature.Edit.UndoCommand import (
+ AddCommand, DelCommand, SetDataCommand, PasteCommand, SortCommand,
+)
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class TableModel(PamhyrTableModel):
+ def get_true_data_row(self, row):
+ if len(self._data.data) > 0:
+ bc = self._data.data[row]
+ else:
+ return 0
+
+ return next(
+ map(
+ lambda e: e[0],
+ filter(
+ lambda e: e[1] == bc,
+ enumerate(self._data._data)
+ )
+ ), 0
+ )
+
+ def data(self, index, role):
+ if role == Qt.TextAlignmentRole:
+ return Qt.AlignHCenter | Qt.AlignVCenter
+
+ if role != Qt.ItemDataRole.DisplayRole:
+ return QVariant()
+
+ row = index.row()
+ column = index.column()
+
+ value = QVariant()
+
+ if 0 <= column < 2:
+ row = self.get_true_data_row(row)
+ v = self._data._data[row][column]
+ if self._data._types[column] == float:
+ value = f"{v:.2f}"
+ elif self._data.header[column] == "time":
+ if self._opt_data == "time":
+ value = timestamp_to_old_pamhyr_date(int(v))
+ else:
+ value = str(datetime.fromtimestamp(v))
+ else:
+ value = f"{v}"
+
+ return value
+
+ 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:
+ row = self.get_true_data_row(row)
+
+ self._undo.push(
+ SetDataCommand(
+ self._data, row, column, value
+ )
+ )
+ except Exception as e:
+ logger.info(e)
+ logger.debug(traceback.format_exc())
+
+ self.dataChanged.emit(index, index)
+ return True
+
+ def add(self, row, parent=QModelIndex()):
+ self.beginInsertRows(parent, row, row - 1)
+
+ self._undo.push(
+ AddCommand(
+ self._data, row
+ )
+ )
+
+ self.endInsertRows()
+ self.layoutChanged.emit()
+
+ def delete(self, rows, parent=QModelIndex()):
+ self.beginRemoveRows(parent, rows[0], rows[-1])
+ _rows = list(
+ map(
+ lambda r: self.get_true_data_row(r),
+ rows
+ )
+ )
+ self._undo.push(
+ DelCommand(
+ self._data, _rows
+ )
+ )
+
+ self.endRemoveRows()
+ self.layoutChanged.emit()
+
+ def sort(self, _reverse, parent=QModelIndex()):
+ self.layoutAboutToBeChanged.emit()
+
+ self._undo.push(
+ SortCommand(
+ self._data, _reverse
+ )
+ )
+
+ self.layoutAboutToBeChanged.emit()
+ self.update()
+
+ def paste(self, row, header, data):
+ if len(data) == 0:
+ return
+
+ self.layoutAboutToBeChanged.emit()
+
+ self._undo.push(
+ PasteCommand(
+ self._data, row,
+ list(
+ map(
+ lambda d: self._data.new_from_data(header, d),
+ data
+ )
+ )
+ )
+ )
+
+ self.layoutAboutToBeChanged.emit()
+ self.update()
+
+ def update(self):
+ # self.auto_sort()
+ self.layoutChanged.emit()
diff --git a/src/View/BoundaryConditionsTemperature/Edit/UndoCommand.py b/src/View/BoundaryConditionsTemperature/Edit/UndoCommand.py
new file mode 100644
index 00000000..f9de4359
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/Edit/UndoCommand.py
@@ -0,0 +1,137 @@
+# 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 -*-
+
+import logging
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+ QMessageBox, QUndoCommand, QUndoStack,
+)
+
+from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
+ import BoundaryConditionTemperature
+
+logger = logging.getLogger()
+
+
+class SetDataCommand(QUndoCommand):
+ def __init__(self, data, index, column, new_value):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._index = index
+ self._column = column
+ self._old = self._data.get_i(self._index)[self._column]
+ self._new = new_value
+
+ def undo(self):
+ self._data._set_i_c_v(self._index, self._column, self._old)
+
+ def redo(self):
+ self._data._set_i_c_v(self._index, self._column, self._new)
+
+
+class AddCommand(QUndoCommand):
+ def __init__(self, data, index):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._index = index
+ self._new = None
+
+ def undo(self):
+ del self._data._data[self._index]
+
+ def redo(self):
+ if self._new is None:
+ self._new = self._data.add(self._index)
+ else:
+ self._data.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+ def __init__(self, data, rows):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._rows = rows
+
+ self._bc = []
+ for row in rows:
+ self._bc.append((row, self._data._data[row]))
+ self._bc.sort()
+
+ def undo(self):
+ for row, el in self._bc:
+ el.set_as_not_deleted()
+
+ def redo(self):
+ for row, el in self._bc:
+ el.set_as_deleted()
+
+
+class PasteCommand(QUndoCommand):
+ def __init__(self, data, row, bcs):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._row = row
+ self._bcs = bcs
+ self._bcs.reverse()
+
+ def undo(self):
+ self._data.delete_i(
+ range(self._row, self._row + len(self._bcs))
+ )
+
+ def redo(self):
+ for bc in self._bcs:
+ self._data.insert(self._row, bc)
+
+
+class SortCommand(QUndoCommand):
+ def __init__(self, data, _reverse):
+ QUndoCommand.__init__(self)
+
+ self._data = data
+ self._reverse = _reverse
+
+ self._old = self._data.data
+ self._indexes = None
+
+ def undo(self):
+ ll = self._data.data
+ self._data.sort(
+ key=lambda x: self._indexes[ll.index(x)]
+ )
+
+ def redo(self):
+ self._data.sort(
+ _reverse=self._reverse,
+ key=lambda x: x[0]
+ )
+ if self._indexes is None:
+ self._indexes = list(
+ map(
+ lambda p: self._old.index(p),
+ self._data.data
+ )
+ )
+ self._old = None
diff --git a/src/View/BoundaryConditionsTemperature/Edit/Window.py b/src/View/BoundaryConditionsTemperature/Edit/Window.py
new file mode 100644
index 00000000..2e658b29
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/Edit/Window.py
@@ -0,0 +1,229 @@
+# 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 logging
+
+from tools import timer, trace
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+from View.Tools.PamhyrWidget import PamhyrWidget
+from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate
+
+from PyQt5.QtGui import (
+ QKeySequence,
+)
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel, QCoreApplication,
+ pyqtSlot, pyqtSignal,
+)
+
+from PyQt5.QtWidgets import (
+ QDialogButtonBox, QPushButton, QLineEdit,
+ QFileDialog, QTableView, QAbstractItemView,
+ QUndoStack, QShortcut, QAction, QItemDelegate,
+ QHeaderView, QDoubleSpinBox, QVBoxLayout,
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.BoundaryConditionsTemperature.Edit.translate import BCETranslate
+from View.BoundaryConditionsTemperature.Edit.Table import TableModel
+from View.BoundaryConditionsTemperature.Edit.Plot import Plot
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class EditBoundaryConditionWindow(PamhyrWindow):
+ _pamhyr_ui = "EditBoundaryConditionsTemperature"
+ _pamhyr_name = "Edit Boundary Conditions Temperature"
+
+ def __init__(self, data=None, study=None, config=None, parent=None):
+ self._data = data
+ trad = BCETranslate()
+
+ name = trad[self._pamhyr_name]
+
+ if self._data is not None:
+ n = self._data.node
+ node_name = next(filter(
+ lambda x: x.id == n, study.river._nodes
+ )).name
+ name += (
+ f" - {study.name} " +
+ f"({node_name})"
+ )
+
+ super(EditBoundaryConditionWindow, self).__init__(
+ title=name,
+ study=study,
+ config=config,
+ trad=trad,
+ parent=parent
+ )
+
+ self._hash_data.append(data)
+
+ self.setup_table()
+ self.setup_plot()
+ self.setup_connections()
+
+ def setup_table(self):
+ table_headers = self._trad.get_dict("table_headers")
+
+ if self._study.is_editable():
+ editable_headers = self._trad.get_dict("table_headers")
+ else:
+ editable_headers = []
+
+ self._data.header = ["time", "temperature"]
+
+ headers = {}
+ for h in self._data.header:
+ headers[h] = table_headers[h]
+
+ self._delegate_time = PamhyrExTimeDelegate(
+ data=self._data,
+ mode=self._study.time_system,
+ parent=self
+ )
+
+ table = self.find(QTableView, "tableView")
+ self._table = TableModel(
+ table_view=table,
+ table_headers=headers,
+ editable_headers=editable_headers,
+ delegates={
+ # "time": self._delegate_time,
+ },
+ data=self._data,
+ undo=self._undo_stack,
+ opt_data=self._study.time_system
+ )
+
+ table.setModel(self._table)
+ table.setSelectionBehavior(QAbstractItemView.SelectRows)
+ table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+ table.setAlternatingRowColors(True)
+
+ def setup_plot(self):
+ self.canvas = MplCanvas(width=5, height=4, dpi=100)
+ self.canvas.setObjectName("canvas")
+ self.toolbar = PamhyrPlotToolbar(
+ self.canvas, self
+ )
+ self.verticalLayout.addWidget(self.toolbar)
+ self.verticalLayout.addWidget(self.canvas)
+
+ self.plot = Plot(
+ canvas=self.canvas,
+ data=self._data,
+ mode=self._study.time_system,
+ trad=self._trad,
+ toolbar=self.toolbar,
+ )
+ self.plot.draw()
+
+ def setup_connections(self):
+ if self._study.is_editable():
+ self.find(QAction, "action_add").triggered.connect(self.add)
+ self.find(QAction, "action_del").triggered.connect(self.delete)
+ self.find(QAction, "action_sort").triggered.connect(self.sort)
+ self._table.dataChanged.connect(self.update)
+
+ def update(self):
+ self.plot.update()
+
+ def index_selected_row(self):
+ table = self.find(QTableView, "tableView")
+ return table.selectionModel()\
+ .selectedRows()[0]\
+ .row()
+
+ def index_selected_rows(self):
+ table = self.find(QTableView, "tableView")
+ return list(
+ # Delete duplicate
+ set(
+ map(
+ lambda i: i.row(),
+ table.selectedIndexes()
+ )
+ )
+ )
+
+ def add(self):
+ rows = self.index_selected_rows()
+ if len(self._data) == 0 or len(rows) == 0:
+ self._table.add(0)
+ else:
+ self._table.add(rows[0])
+
+ self.plot.update()
+
+ def delete(self):
+ rows = self.index_selected_rows()
+ if len(rows) == 0:
+ return
+
+ self._table.delete(rows)
+ self.plot.update()
+
+ def sort(self):
+ self._table.sort(False)
+ self.plot.update()
+
+ def _copy(self):
+ rows = self.index_selected_rows()
+
+ table = []
+ table.append(self._data.header)
+
+ data = self._data.data
+ for row in rows:
+ table.append(list(data[row]))
+
+ self.copyTableIntoClipboard(table)
+
+ def _paste(self):
+ header, data = self.parseClipboardTable()
+
+ logger.debug(f"paste: h:{header}, d:{data}")
+
+ if len(data) == 0:
+ return
+
+ row = 0
+ rows = self.index_selected_rows()
+ if len(rows) != 0:
+ row = rows[0]
+
+ self._table.paste(row, header, data)
+ self.plot.update()
+
+ def _undo(self):
+ self._table.undo()
+ self.plot.update()
+
+ def _redo(self):
+ self._table.redo()
+ self.plot.update()
diff --git a/src/View/BoundaryConditionsTemperature/Edit/translate.py b/src/View/BoundaryConditionsTemperature/Edit/translate.py
new file mode 100644
index 00000000..4e6e76cd
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/Edit/translate.py
@@ -0,0 +1,40 @@
+# 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
+
+from View.BoundaryCondition.translate import BCTranslate
+
+_translate = QCoreApplication.translate
+
+
+class BCETranslate(BCTranslate):
+ def __init__(self):
+ super(BCETranslate, self).__init__()
+
+ self._dict["Edit Boundary Conditions Temperature"] = _translate(
+ "BoundaryConditionsTemperature",
+ "Edit boundary conditions Temperature"
+ )
+
+ self._sub_dict["table_headers"] = {
+ "time": self._dict["time"],
+ "temperature": self._dict["unit_temperature"],
+ }
diff --git a/src/View/BoundaryConditionsTemperature/Table.py b/src/View/BoundaryConditionsTemperature/Table.py
new file mode 100644
index 00000000..76f95e83
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/Table.py
@@ -0,0 +1,219 @@
+# 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.BoundaryConditionsTemperature.UndoCommand import (
+ SetNodeCommand, SetNameCommand,
+ AddCommand, DelCommand
+)
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+ def __init__(self, data=None, mode="node", tab="",
+ trad=None, parent=None):
+ super(ComboBoxDelegate, self).__init__(parent)
+
+ self._data = data
+ self._mode = mode
+ self._tab = tab
+ self._trad = trad
+
+ def createEditor(self, parent, option, index):
+ self.editor = QComboBox(parent)
+
+ if self._mode == "node":
+ self.editor.addItems(
+ [self._trad["not_associated"]] +
+ self._data.nodes_names()
+ )
+
+ self.editor.setCurrentText(index.data(Qt.DisplayRole))
+ return self.editor
+
+ def setEditorData(self, editor, index):
+ value = index.data(Qt.DisplayRole)
+ self.editor.currentTextChanged.connect(self.currentItemChanged)
+
+ def setModelData(self, editor, model, index):
+ text = str(editor.currentText())
+ model.setData(index, text)
+ editor.close()
+ editor.deleteLater()
+
+ def updateEditorGeometry(self, editor, option, index):
+ r = QRect(option.rect)
+ if self.editor.windowFlags() & Qt.Popup:
+ if editor.parent() is not None:
+ r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
+ editor.setGeometry(r)
+
+ @pyqtSlot()
+ def currentItemChanged(self):
+ self.commitData.emit(self.sender())
+
+
+class TableModel(PamhyrTableModel):
+ def __init__(self, bc_list=None, trad=None, **kwargs):
+ self._trad = trad
+ self._bc_list = bc_list
+
+ super(TableModel, self).__init__(trad=trad, **kwargs)
+
+ def _setup_lst(self):
+ self._lst = self._bc_list.lst
+
+ def get(self, row):
+ if row < 0 or row >= len(self._lst):
+ return None
+ return self._lst[row]
+
+ def _global_row(self, row):
+ bc = self.get(row)
+ if bc is None:
+ return None
+ return self._bc_list.index(bc)
+
+ def rowCount(self, parent=QModelIndex()):
+ 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] == "node":
+ n = self._lst[row].node
+ if n is None:
+ return self._trad["not_associated"]
+ tmp = next(filter(lambda x: x.id == n, self._data._nodes), None)
+ if tmp is not None:
+ return tmp.name
+ else:
+ return self._trad["not_associated"]
+
+ 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] == "name":
+ global_row = self._global_row(row)
+ self._undo.push(
+ SetNameCommand(
+ self._bc_list, global_row, value
+ )
+ )
+ elif self._headers[column] == "node":
+ global_row = self._global_row(row)
+ node = next(
+ filter(lambda n: n.name == value, self._data.nodes()),
+ None
+ )
+ self._undo.push(
+ SetNodeCommand(
+ self._bc_list, global_row, node
+ )
+ )
+
+ 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()):
+ row = len(self._lst)
+ self.beginInsertRows(parent, row, row)
+ self._undo.push(
+ AddCommand(
+ self._bc_list, len(self._bc_list)
+ )
+ )
+ self._setup_lst()
+ self.endInsertRows()
+ self.layoutChanged.emit()
+
+ def delete(self, rows, parent=QModelIndex()):
+ self.beginRemoveRows(parent, rows[0], rows[-1])
+
+ row_by_bc = {
+ id(bc): row for row, bc in enumerate(self._bc_list._lst)
+ }
+ global_rows = [
+ row_by_bc[id(self._lst[row])]
+ for row in rows
+ if 0 <= row < len(self._lst)
+ ]
+ self._undo.push(
+ DelCommand(
+ self._bc_list, global_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/BoundaryConditionsTemperature/UndoCommand.py b/src/View/BoundaryConditionsTemperature/UndoCommand.py
new file mode 100644
index 00000000..6b65d7de
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/UndoCommand.py
@@ -0,0 +1,100 @@
+# 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.BoundaryConditionsTemperature.BoundaryConditionTemperature \
+ import BoundaryConditionTemperature
+from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
+ import BoundaryConditionsTemperatureList
+
+
+class SetNodeCommand(QUndoCommand):
+ def __init__(self, bcs, index, node):
+ QUndoCommand.__init__(self)
+
+ self._bcs = bcs
+ self._index = index
+ self._old = self._bcs.get(self._index).node
+ self._new = node.id if node is not None else None
+
+ def undo(self):
+ self._bcs.get(self._index).node = self._old
+
+ def redo(self):
+ self._bcs.get(self._index).node = self._new
+
+
+class SetNameCommand(QUndoCommand):
+ def __init__(self, bcs, index, name):
+ QUndoCommand.__init__(self)
+
+ self._bcs = bcs
+ self._index = index
+ self._name = name
+ self._old = self._bcs.get(self._index).name
+ self._new = self._name
+
+ def undo(self):
+ self._bcs.get(self._index).name = self._old
+
+ def redo(self):
+ self._bcs.get(self._index).name = self._new
+
+
+class AddCommand(QUndoCommand):
+ def __init__(self, bcs, index):
+ QUndoCommand.__init__(self)
+
+ self._bcs = bcs
+ self._index = index
+ self._new = None
+
+ def undo(self):
+ self._bcs.delete_i([self._index])
+
+ def redo(self):
+ if self._new is None:
+ self._new = self._bcs.new(self._index)
+ else:
+ self._bcs.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+ def __init__(self, bcs, rows):
+ QUndoCommand.__init__(self)
+
+ self._bcs = bcs
+ self._rows = rows
+
+ self._bc = []
+ for row in rows:
+ self._bc.append((row, self._bcs._lst[row]))
+ self._bc.sort()
+
+ def undo(self):
+ for row, el in self._bc:
+ self._bcs.insert(row, el)
+
+ def redo(self):
+ self._bcs.delete_i(self._rows)
diff --git a/src/View/BoundaryConditionsTemperature/Window.py b/src/View/BoundaryConditionsTemperature/Window.py
new file mode 100644
index 00000000..715cc30f
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/Window.py
@@ -0,0 +1,192 @@
+# 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 logging
+
+from tools import trace, timer, logger_exception
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+
+from PyQt5.QtGui import (
+ QKeySequence,
+)
+
+from PyQt5.QtCore import (
+ Qt, QVariant, QAbstractTableModel,
+ QCoreApplication, QModelIndex, pyqtSlot,
+ QRect,
+)
+
+from PyQt5.QtWidgets import (
+ QDialogButtonBox, QPushButton, QLineEdit,
+ QFileDialog, QTableView, QAbstractItemView,
+ QUndoStack, QShortcut, QAction, QItemDelegate,
+ QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
+ QWidget,
+)
+
+from View.BoundaryConditionsTemperature.Table import (
+ TableModel, ComboBoxDelegate
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.BoundaryConditionsTemperature.translate import BCTemperatureTranslate
+from View.BoundaryConditionsTemperature.Edit.Window \
+ import EditBoundaryConditionWindow
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class BoundaryConditionsTemperatureWindow(PamhyrWindow):
+ _pamhyr_ui = "BoundaryConditionsTemperature"
+ _pamhyr_name = "Boundary conditions temperature"
+
+ def __init__(self, data=None, study=None,
+ config=None, parent=None):
+ self._data = data
+ trad = BCTemperatureTranslate()
+
+ name = (
+ trad[self._pamhyr_name] +
+ " - " + study.name
+ )
+
+ super(BoundaryConditionsTemperatureWindow, self).__init__(
+ title=name,
+ study=study,
+ config=config,
+ trad=trad,
+ parent=parent
+ )
+
+ self._bcs = self._study.river.boundary_conditions_temperature
+
+ self.setup_graph()
+ self.setup_table()
+ self.setup_connections()
+
+ def setup_table(self):
+ self._delegate_node = ComboBoxDelegate(
+ trad=self._trad,
+ data=self._study.river,
+ mode="node",
+ parent=self
+ )
+
+ if self._study.is_editable():
+ editable_headers = self._trad.get_dict("table_headers")
+ else:
+ editable_headers = []
+
+ table = self.find(QTableView, f"tableView")
+ self._table = TableModel(
+ table_view=table,
+ table_headers=self._trad.get_dict("table_headers"),
+ editable_headers=editable_headers,
+ delegates={
+ "node": self._delegate_node,
+ },
+ trad=self._trad,
+ bc_list=self._study.river.boundary_conditions_temperature,
+ undo=self._undo_stack,
+ data=self._study.river
+ )
+ table.setModel(self._table)
+ table.setSelectionBehavior(QAbstractItemView.SelectRows)
+ table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+ table.setAlternatingRowColors(True)
+
+ def setup_graph(self):
+ self.graph_widget = GraphWidget(
+ self._study.river,
+ min_size=None, size=(200, 200),
+ only_display=True,
+ parent=self
+ )
+ self.graph_layout = self.find(QVBoxLayout, "verticalLayout")
+ self.graph_layout.addWidget(self.graph_widget)
+
+ def setup_connections(self):
+ if self._study.is_editable():
+ self.find(QAction, "action_add").triggered.connect(self.add)
+ self.find(QAction, "action_del").triggered.connect(self.delete)
+
+ self.find(QAction, "action_edit").triggered.connect(self.edit)
+
+ def index_selected_row(self):
+ table = self.find(QTableView, f"tableView")
+ return table.selectionModel()\
+ .selectedRows()[0]\
+ .row()
+
+ def index_selected_rows(self):
+ table = self.find(QTableView, f"tableView")
+ return list(
+ # Delete duplicate
+ set(
+ map(
+ lambda i: i.row(),
+ table.selectedIndexes()
+ )
+ )
+ )
+
+ def add(self):
+ self._table.add(self._table.rowCount())
+
+ 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._table.get(row)
+
+ if data.node is None:
+ continue
+
+ if self.sub_window_exists(
+ EditBoundaryConditionWindow,
+ data=[self._study, None, data]
+ ):
+ continue
+
+ win = EditBoundaryConditionWindow(
+ data=data,
+ study=self._study,
+ parent=self
+ )
+ win.show()
diff --git a/src/View/BoundaryConditionsTemperature/translate.py b/src/View/BoundaryConditionsTemperature/translate.py
new file mode 100644
index 00000000..8978a42e
--- /dev/null
+++ b/src/View/BoundaryConditionsTemperature/translate.py
@@ -0,0 +1,37 @@
+# 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 BCTemperatureTranslate(MainTranslate):
+ def __init__(self):
+ super(BCTemperatureTranslate, self).__init__()
+
+ self._dict["Boundary conditions temperature"] = _translate(
+ "BoundaryConditionsTemperature", "Boundary conditions temperature"
+ )
+
+ self._sub_dict["table_headers"] = {
+ "name": self._dict["name"],
+ "node": _translate("BoundaryCondition", "Node"),
+ }
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 064f4df4..7a25f529 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -110,6 +110,10 @@ from View.InitialConditionsTemperature.Window import (
InitialConditionsTemperatureWindow
)
+from View.BoundaryConditionsTemperature.Window import (
+ BoundaryConditionsTemperatureWindow
+)
+
from Solver.Solvers import GenericSolver
from Model.Results.Results import Results
@@ -276,6 +280,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# Menu action
"action_menu_initial_conditions_temperature":
self.open_initial_conditions_temperature,
+ "action_menu_boundary_conditions_temperature":
+ self.open_boundary_conditions_temperature,
"action_menu_dif": self.open_dif,
"action_menu_d90": self.open_d90,
"action_menu_pollutants": self.open_pollutants,
@@ -1031,8 +1037,24 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# SUB WINDOWS #
###############
- def open_initial_conditions_temperature(self):
+ def open_boundary_conditions_temperature(self):
+ river = self._study.river
+ bclist = river.boundary_conditions_temperature.lst
+ if self.sub_window_exists(
+ BoundaryConditionsTemperatureWindow,
+ data=[self._study, None, bclist]
+ ):
+ return
+
+ bound = BoundaryConditionsTemperatureWindow(
+ study=self._study,
+ parent=self,
+ data=bclist,
+ )
+ bound.show()
+
+ def open_initial_conditions_temperature(self):
iclist = self._study.river.ic_temperature.lst
if len(iclist) != 0:
ic_default = iclist[0]
@@ -2178,6 +2200,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
logger.debug("No study open for sql debuging...")
return
+ # todo : gérer le cas où le dossier a un espace dans le nom
+ # (ne veut pas ouvrir sqlitebrowser)
+
file = self._study.filename
_ = subprocess.Popen(
f"sqlitebrowser {file}",
diff --git a/src/View/ui/BoundaryConditionsTemperature.ui b/src/View/ui/BoundaryConditionsTemperature.ui
new file mode 100644
index 00000000..fdf40608
--- /dev/null
+++ b/src/View/ui/BoundaryConditionsTemperature.ui
@@ -0,0 +1,128 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 861
+ 498
+
+
+
+
+ 0
+ 0
+
+
+
+ MainWindow
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
-
+
+
+
+ 300
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+ false
+
+
+
+ ressources/add.pngressources/add.png
+
+
+ Add
+
+
+ Add a new boundary condition or punctual contribution
+
+
+ Ctrl+N
+
+
+
+
+
+ ressources/del.pngressources/del.png
+
+
+ Delete
+
+
+ Delete current selected rows
+
+
+ Ctrl+D
+
+
+
+
+
+ ressources/edit.pngressources/edit.png
+
+
+ Edit
+
+
+ Edit boundary condition or punctual contribution
+
+
+ Ctrl+E
+
+
+
+
+
+
diff --git a/src/View/ui/EditBoundaryConditionsTemperature.ui b/src/View/ui/EditBoundaryConditionsTemperature.ui
new file mode 100644
index 00000000..bec71444
--- /dev/null
+++ b/src/View/ui/EditBoundaryConditionsTemperature.ui
@@ -0,0 +1,125 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 450
+
+
+
+
+ 0
+ 0
+
+
+
+ MainWindow
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
-
+
+
+
+ 0
+ 200
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+ false
+
+
+
+ ressources/add.pngressources/add.png
+
+
+ Add
+
+
+ Add a new point in boundary condition or punctual contribution
+
+
+ Ctrl+N
+
+
+
+
+
+ ressources/del.pngressources/del.png
+
+
+ Delete
+
+
+ Delete current selected rows
+
+
+ Ctrl+D
+
+
+
+
+
+ ressources/sort_1-9.pngressources/sort_1-9.png
+
+
+ Sort
+
+
+ Sort points
+
+
+
+
+
+
diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui
index 5ea27ab1..93d72519 100644
--- a/src/View/ui/MainWindow.ui
+++ b/src/View/ui/MainWindow.ui
@@ -838,11 +838,17 @@
+
+ false
+
DIF ?
+
+ false
+
Lateral contributions ?