From 2cbfaec0c283ee890dbe7a58ccc1d71e143730a4 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 26 Jun 2026 19:02:49 +0200 Subject: [PATCH] Boundary Conditions: interface for temperature's boundary conditions --- .../BoundaryConditionTemperature.py | 478 ++++++++++++++++++ .../BoundaryConditionsTemperatureList.py | 71 +++ src/Model/River.py | 17 + .../Edit/Plot.py | 105 ++++ .../Edit/Table.py | 185 +++++++ .../Edit/UndoCommand.py | 137 +++++ .../Edit/Window.py | 229 +++++++++ .../Edit/translate.py | 40 ++ .../BoundaryConditionsTemperature/Table.py | 219 ++++++++ .../UndoCommand.py | 100 ++++ .../BoundaryConditionsTemperature/Window.py | 192 +++++++ .../translate.py | 37 ++ src/View/MainWindow.py | 27 +- src/View/ui/BoundaryConditionsTemperature.ui | 128 +++++ .../ui/EditBoundaryConditionsTemperature.ui | 125 +++++ src/View/ui/MainWindow.ui | 6 + 16 files changed, 2095 insertions(+), 1 deletion(-) create mode 100644 src/Model/BoundaryConditionsTemperature/BoundaryConditionTemperature.py create mode 100644 src/Model/BoundaryConditionsTemperature/BoundaryConditionsTemperatureList.py create mode 100644 src/View/BoundaryConditionsTemperature/Edit/Plot.py create mode 100644 src/View/BoundaryConditionsTemperature/Edit/Table.py create mode 100644 src/View/BoundaryConditionsTemperature/Edit/UndoCommand.py create mode 100644 src/View/BoundaryConditionsTemperature/Edit/Window.py create mode 100644 src/View/BoundaryConditionsTemperature/Edit/translate.py create mode 100644 src/View/BoundaryConditionsTemperature/Table.py create mode 100644 src/View/BoundaryConditionsTemperature/UndoCommand.py create mode 100644 src/View/BoundaryConditionsTemperature/Window.py create mode 100644 src/View/BoundaryConditionsTemperature/translate.py create mode 100644 src/View/ui/BoundaryConditionsTemperature.ui create mode 100644 src/View/ui/EditBoundaryConditionsTemperature.ui 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 + + + + + + + + + + + + + + + + + 0 + 0 + 861 + 22 + + + + + + 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 + + + + + + + + + + + + + + + + + 0 + 0 + 800 + 22 + + + + + + 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 ?