diff --git a/src/View/CheckList/WindowAdisTS.py b/src/View/CheckList/WindowAdisTS.py index b0780801..bc95fb5b 100644 --- a/src/View/CheckList/WindowAdisTS.py +++ b/src/View/CheckList/WindowAdisTS.py @@ -53,12 +53,13 @@ class CheckListWindowAdisTS(PamhyrWindow): def __init__(self, autorun: bool = True, study=None, config=None, - solver=None, parent=None, mage_rep=None): + solver=None, parent=None, mage_rep=None, mode="adists"): trad = CheckListTranslate() self._autorun = autorun self._solver = solver self._mage_rep = mage_rep + self._mode = mode name = trad[self._pamhyr_name] + " - " + study.name @@ -73,6 +74,8 @@ class CheckListWindowAdisTS(PamhyrWindow): # Add solver to hash computation data self._hash_data.append(self._solver) + self._hash_data.append(self._mage_rep) + self._hash_data.append(self._mode) self._checker_list = ( self._study.checkers() + @@ -169,7 +172,7 @@ class CheckListWindowAdisTS(PamhyrWindow): if not errors: self.find(QPushButton, "pushButton_ok").setEnabled(True) if self._autorun: - self._parent.solver_log_adists(self._solver, self._mage_rep) + self.open_solver_log() self.end() self.update_statusbar() @@ -196,5 +199,11 @@ class CheckListWindowAdisTS(PamhyrWindow): self.end() def accept(self): - self._parent.solver_log_adists(self._solver, self._mage_rep) + self.open_solver_log() # self.end() + + def open_solver_log(self): + if self._mode == "adistt": + self._parent.solver_log_adistt(self._solver, self._mage_rep) + else: + self._parent.solver_log_adists(self._solver, self._mage_rep) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 2243e8e8..b855fef7 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -103,6 +103,7 @@ from View.RunSolver.WindowAdisTS import ( from View.CheckList.Window import CheckListWindow from View.CheckList.WindowAdisTS import CheckListWindowAdisTS from View.Results.WindowAdisTS import ResultsWindowAdisTS +from View.Results.WindowAdisTT import ResultsWindowAdisTT from View.Results.ReadingResultsDialog import ReadingResultsDialog from View.Debug.Window import ReplWindow, TimerWindow @@ -297,9 +298,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_d90": self.open_d90, "action_menu_pollutants": self.open_pollutants, "action_menu_run_adists": - lambda: self.select_run_solver_adists(mode="adists"), + self.select_run_solver_adists, "action_menu_run_adistt": - lambda: self.select_run_solver_adists(mode="adistt"), + self.select_run_solver_adistt, "action_menu_output_rk": self.open_output_rk_adists, "action_menu_config": self.open_configure, "action_menu_new": self.open_new_study, @@ -1570,7 +1571,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if run.exec(): self.run_solver(run.solver) - def select_run_solver_adists(self, mode="adists"): + def select_run_solver_adists(self): if self._study is None: return @@ -1586,9 +1587,27 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): parent=self ) if run.exec(): - self.run_solver_adists(run.solver, run.mage_rep, mode=mode) + self.run_solver_adists(run.solver, run.mage_rep) - def run_solver_adists(self, solver, mage_rep, mode="adists"): + def select_run_solver_adistt(self): + if self._study is None: + return + + run = SelectSolverWindowAdisTT( + study=self._study, + config=self.conf, + parent=self + ) + if run.exec(): + self.run_solver_adistt(run.solver, run.mage_rep) + + def run_solver_adists(self, solver, mage_rep): + self._run_solver_adis(solver, mage_rep, mode="adists") + + def run_solver_adistt(self, solver, mage_rep): + self._run_solver_adis(solver, mage_rep, mode="adistt") + + def _run_solver_adis(self, solver, mage_rep, mode): if self._study is None: return @@ -1598,7 +1617,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self._study, self.conf, solver, - mage_rep + mage_rep, + mode, ] ): return @@ -1609,6 +1629,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): solver=solver, parent=self, mage_rep=mage_rep, + mode=mode, ) check.show() @@ -1644,6 +1665,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) sol.show() + def solver_log_adistt(self, solver, mage_rep): + sol = SolverLogWindowAdisTT( + study=self._study, + config=self.conf, + solver=solver, + parent=self, + mage_rep=mage_rep, + ) + sol.show() + def solver_log(self, solver): sol = SolverLogWindow( study=self._study, @@ -1763,6 +1794,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) def open_solver_results_adists(self, solver, results=None): + self._open_solver_results_adis( + solver, ResultsWindowAdisTS, results=results + ) + + def open_solver_results_adistt(self, solver, results=None): + self._open_solver_results_adis( + solver, ResultsWindowAdisTT, results=results + ) + + def _open_solver_results_adis(self, solver, window_class, results=None): def reading_fn(): self._tmp_results = results @@ -1804,7 +1845,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # Windows already opened if self.sub_window_exists( - ResultsWindowAdisTS, + window_class, data=[ self._study, None, # No config @@ -1815,7 +1856,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ): return - res = ResultsWindowAdisTS( + res = window_class( study=self._study, solver=solver, results=results, diff --git a/src/View/Results/PlotSedAdis.py b/src/View/Results/PlotSedAdis.py index c22c3ad5..4f6f4272 100644 --- a/src/View/Results/PlotSedAdis.py +++ b/src/View/Results/PlotSedAdis.py @@ -63,6 +63,9 @@ class PlotAdis(PamhyrPlot): self.label["C"] = _translate("Results", "Concentration") self.label_max["C"] = _translate("Results", "Max Concentration") self.label_min["C"] = _translate("Results", "Min Concentration") + self.label["T"] = _translate("Results", "Temperature") + self.label_max["T"] = _translate("Results", "Max Temperature") + self.label_min["T"] = _translate("Results", "Min Temperature") if self._type_pol[pol_id[0]] == -1: # Total self.label["M"] = _translate("Results", "Thickness") self.label_max["M"] = _translate("Results", "Max Thickness") @@ -75,6 +78,7 @@ class PlotAdis(PamhyrPlot): self.label_y = self._available_values_y["unit_"+self._key] self.val_id = {} self.val_id["C"] = 0 + self.val_id["T"] = 0 self.val_id["G"] = 1 self.val_id["M"] = 2 self.val_id["D"] = 3 @@ -205,9 +209,12 @@ class PlotAdis_dx(PlotAdis): self._current_timestamp, "pols" )[pol_id][self.val_id[self._key]], reach.profiles)) + label = self.label[self._key] + if name != "TEM": + label += (" "+name) self._lines.append(self.canvas.axes.plot( x, y, - label=self.label[self._key]+" "+name, + label=label, color=self.colors[pol_id % len(self.colors)], **self.plot_default_kargs )[0]) diff --git a/src/View/Results/TableAdisTT.py b/src/View/Results/TableAdisTT.py new file mode 100644 index 00000000..a1956ad8 --- /dev/null +++ b/src/View/Results/TableAdisTT.py @@ -0,0 +1,128 @@ +# TableAdisTT.py -- Pamhyr +# Copyright (C) 2023-2026 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 timer, trace + +from PyQt5.QtGui import ( + QKeySequence, QColor +) +from PyQt5.QtCore import ( + Qt, QAbstractTableModel, QModelIndex, + QVariant, pyqtSlot, QCoreApplication, +) +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, + QStyledItemDelegate, QLineEdit, QAbstractItemView, + QComboBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel +from View.Results.translate import * + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class TableModel(PamhyrTableModel): + def _setup_lst(self): + _river = self._data.river + self._timestamp = min(list(self._data.get("timestamps"))) + if self._opt_data == "reach": + self._lst = _river.reachs + elif self._opt_data == "profile": + self._lst = _river.reach(0).profiles + elif self._opt_data == "raw_data": + self._lst = _river.reach(0).profiles + + def __init__(self, type_pol, **kwargs): + self._type_pol = type_pol + super(TableModel, self).__init__(**kwargs) + + def data(self, index, role=Qt.DisplayRole): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._opt_data == "reach": + if self._headers[column] == "name": + v = self._lst[row].name + return str(v) + elif self._opt_data == "profile": + if self._headers[column] == "name": + v = self._lst[row].name + return str(v) + elif self._headers[column] == "rk": + v = self._lst[row].rk + return f"{v:.4f}" + elif self._opt_data == "raw_data": + p = self._lst[row] + if self._headers[column] == "name": + if p.name == "": + return f"{p.rk:.4f}" + return f"{p.name}" + tmp_list = self._data.pollutants_list.copy() + tmp_list.remove("total_sediment") + tmp_list2 = self._data.pollutants_list.copy() + for pol in tmp_list: + pol_index = tmp_list2.index(pol) + if self._headers[column] == pol + " Concentration": + v = self._lst[row].get_ts_key( + self._timestamp, "pols")[pol_index][0] + return f"{v:.4f}" + if self._headers[column] == pol + " Temperature": + v = self._lst[row].get_ts_key( + self._timestamp, "pols")[pol_index][0] + return f"{v:.4f}" + if self._headers[column] == pol + " Mass": + if self._type_pol[pol_index] == 7: + m1 = self._lst[row].get_ts_key( + self._timestamp, "pols")[pol_index][1] + m2 = self._lst[row].get_ts_key( + self._timestamp, "pols")[pol_index][2] + m3 = self._lst[row].get_ts_key( + self._timestamp, "pols")[pol_index][3] + v = m1 + m2 + m3 + return f"{v:.4f}" + else: + return "" + + return QVariant() + + def update(self, reach): + _river = self._data.river + + if self._opt_data == "reach": + self._lst = _river.reachs + elif self._opt_data == "profile" or self._opt_data == "raw_data": + self._lst = _river.reach(reach).profiles + + self.layoutChanged.emit() + + @property + def timestamp(self): + return self._timestamp + + def set_timestamp(self, timestamp): + self._timestamp = timestamp + self.layoutChanged.emit() diff --git a/src/View/Results/WindowAdisTT.py b/src/View/Results/WindowAdisTT.py new file mode 100644 index 00000000..bdb73787 --- /dev/null +++ b/src/View/Results/WindowAdisTT.py @@ -0,0 +1,756 @@ +# WindowAdisTT.py -- Pamhyr +# Copyright (C) 2023-2026 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import os +import csv +import logging + +from datetime import datetime +from tools import trace, timer, logger_exception + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5.QtGui import ( + QKeySequence, QIcon, QPixmap, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QItemSelectionModel, QTimer, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, + QSlider, QLabel, QWidget, QGridLayout, +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.Results.PlotSedAdis import PlotAdis_dx, PlotAdis_dt + +from View.Results.CustomPlot.Plot import CustomPlot +from View.Results.CustomExport.CustomExportAdis import ( + CustomExportAdisDialog, +) + +from View.Results.TableAdisTT import TableModel +from View.Results.translate import ResultsTranslate + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class ResultsWindowAdisTT(PamhyrWindow): + _pamhyr_ui = "ResultsAdisTT" + _pamhyr_name = "Results" + + def _path_file(self, filename): + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", "ressources", filename + ) + ) + + def __init__(self, study=None, config=None, + solver=None, results=None, + parent=None): + self._solver = solver + self._results = results + self.pollutant_label = ["None"] + + pollutants_headers = self._results.pollutants_list.copy() + + trad = ResultsTranslate(pollutants_headers) + name = ( + trad[self._pamhyr_name] + " - " + + study.name + " - " + + self._solver.name + ) + + super(ResultsWindowAdisTT, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + self._hash_data.append(self._solver) + self._hash_data.append(self._results._repertory) + self._hash_data.append(self._results._name) + + self._additional_plot = {} + self._current_pol_id = [1] + self._reach_id = 0 + self._profile_id = 0 + + try: + self._timestamps = sorted(list(self._results.get("timestamps"))) + self.set_type_pol() + self.setup_slider() + self.setup_table() + self.setup_plots() + self.setup_statusbar() + self.setup_connections() + self.update_table_selection_reach(self._reach_id) + self.update_table_selection_profile(self._profile_id) + except Exception as e: + logger_exception(e) + return + + def set_type_pol(self): + self._type_pol = [] + self._pol_id_dict = {} + tmp_list = self._results.river.reach(0).profiles + for pol_index in range(self._results.nb_pollutants): + pol_name = self._results.pollutants_list[pol_index] + if pol_name == "total_sediment": + self._type_pol.append(-1) + else: + self._type_pol.append(len( + tmp_list[0].get_ts_key( + self._timestamps[0], "pols")[pol_index]) + ) + self._pol_id_dict[pol_name] = pol_index + + def setup_table(self): + self._table = {} + for t in ["reach", "profile", "raw_data"]: + table = self.find(QTableView, f"tableView_{t}") + self._table[t] = TableModel( + self._type_pol, + table_view=table, + table_headers=self._trad.get_dict(f"table_headers_{t}"), + data=self._results, + undo=self._undo_stack, + opt_data=t, + ) + self._table[t]._timestamp = self._timestamps[ + self._slider_time.value()] + + def setup_slider(self): + self._slider_time = self.find(QSlider, f"horizontalSlider_time") + self._slider_time.setMaximum(len(self._timestamps) - 1) + self._slider_time.setValue(len(self._timestamps) - 1) + + self._icon_start = QIcon() + self._icon_start.addPixmap( + QPixmap(self._path_file("media-playback-start.png")) + ) + + self._icon_pause = QIcon() + self._icon_pause.addPixmap( + QPixmap(self._path_file("media-playback-pause.png")) + ) + self._button_play = self.find(QPushButton, f"playButton") + self._button_play.setIcon(self._icon_start) + self._button_back = self.find(QPushButton, f"backButton") + self._button_next = self.find(QPushButton, f"nextButton") + self._button_first = self.find(QPushButton, f"firstButton") + self._button_last = self.find(QPushButton, f"lastButton") + self._timer = QTimer(self) + + def setup_plots(self): + self.canvas_cdt = MplCanvas(width=5, height=4, dpi=100) + self.canvas_cdt.setObjectName("canvas_cdt") + self.toolbar_cdt = PamhyrPlotToolbar( + self.canvas_cdt, self, items=[ + "home", "move", "zoom", "save", + "iso", "back/forward" + ] + ) + self.plot_layout_cdt = self.find( + QVBoxLayout, "verticalLayout_concentration_dt") + self.plot_layout_cdt.addWidget(self.toolbar_cdt) + self.plot_layout_cdt.addWidget(self.canvas_cdt) + + self.plot_cdt = PlotAdis_dt( + canvas=self.canvas_cdt, + results=self._results, + reach_id=self._reach_id, + profile_id=self._profile_id, + pol_id=self._current_pol_id, + key="T", + type_pol=self._type_pol, + trad=self._trad, + toolbar=self.toolbar_cdt + ) + self.plot_cdt.draw() + + self.canvas_cdx = MplCanvas(width=5, height=4, dpi=100) + self.canvas_cdx.setObjectName("canvas_cdx") + self.toolbar_cdx = PamhyrPlotToolbar( + self.canvas_cdx, self, items=[ + "home", "move", "zoom", "save", + "iso", "back/forward" + ] + ) + self.plot_layout_cdx = self.find( + QVBoxLayout, "verticalLayout_concentration_dx") + self.plot_layout_cdx.addWidget(self.toolbar_cdx) + self.plot_layout_cdx.addWidget(self.canvas_cdx) + + self.plot_cdx = PlotAdis_dx( + canvas=self.canvas_cdx, + results=self._results, + reach_id=self._reach_id, + profile_id=self._profile_id, + pol_id=self._current_pol_id, + key="T", + type_pol=self._type_pol, + trad=self._trad, + toolbar=self.toolbar_cdx + ) + self.plot_cdx.draw() + + # The AdisTT window only displays temperature plots. The code below + # belongs to the sediment/pollutant result window and its layouts are + # intentionally absent from ResultsAdisTT.ui. + return + + self.canvas_mdx = MplCanvas(width=5, height=4, dpi=100) + self.canvas_mdx.setObjectName("canvas_mdx") + self.toolbar_mdx = PamhyrPlotToolbar( + self.canvas_mdx, self, items=[ + "home", "move", "zoom", "save", + "iso", "back/forward" + ] + ) + self.plot_layout_mdx = self.find( + QVBoxLayout, "verticalLayout_mass_dx2") + self.plot_layout_mdx.addWidget(self.toolbar_mdx) + self.plot_layout_mdx.addWidget(self.canvas_mdx) + + self.plot_mdx = PlotAdis_dx( + canvas=self.canvas_mdx, + results=self._results, + reach_id=self._reach_id, + profile_id=self._profile_id, + pol_id=self._current_pol_id, + key="M", + type_pol=self._type_pol, + trad=self._trad, + toolbar=self.toolbar_mdx + ) + + self.plot_mdx.draw() + + self.canvas_mdt = MplCanvas(width=5, height=4, dpi=100) + self.canvas_mdt.setObjectName("canvas_mdt") + self.toolbar_mdt = PamhyrPlotToolbar( + self.canvas_mdt, self, items=[ + "home", "move", "zoom", "save", + "iso", "back/forward" + ] + ) + self.plot_layout_mdt = self.find( + QVBoxLayout, "verticalLayout_mass_dt2") + self.plot_layout_mdt.addWidget(self.toolbar_mdt) + self.plot_layout_mdt.addWidget(self.canvas_mdt) + + self.plot_mdt = PlotAdis_dt( + canvas=self.canvas_mdt, + results=self._results, + reach_id=self._reach_id, + profile_id=self._profile_id, + pol_id=self._current_pol_id, + key="M", + type_pol=self._type_pol, + trad=self._trad, + toolbar=self.toolbar_mdt + ) + + self.plot_mdt.draw() + + # Thickness + + self.canvas_tdx = MplCanvas(width=5, height=4, dpi=100) + self.canvas_tdx.setObjectName("canvas_tdx") + self.toolbar_tdx = PamhyrPlotToolbar( + self.canvas_tdx, self, items=[ + "home", "move", "zoom", "save", + "iso", "back/forward" + ] + ) + self.plot_layout_tdx = self.find( + QVBoxLayout, "verticalLayout_tdx") + self.plot_layout_tdx.addWidget(self.toolbar_tdx) + self.plot_layout_tdx.addWidget(self.canvas_tdx) + + self.plot_tdx = PlotAdis_dx( + canvas=self.canvas_tdx, + results=self._results, + reach_id=self._reach_id, + profile_id=self._profile_id, + pol_id=[0], + key="M", + type_pol=self._type_pol, + trad=self._trad, + toolbar=self.toolbar_tdx + ) + + self.plot_tdx.draw() + + self.canvas_tdt = MplCanvas(width=5, height=4, dpi=100) + self.canvas_tdt.setObjectName("canvas_tdt") + self.toolbar_tdt = PamhyrPlotToolbar( + self.canvas_tdt, self, items=[ + "home", "move", "zoom", "save", + "iso", "back/forward" + ] + ) + self.plot_layout_tdt = self.find( + QVBoxLayout, "verticalLayout_tdt") + self.plot_layout_tdt.addWidget(self.toolbar_tdt) + self.plot_layout_tdt.addWidget(self.canvas_tdt) + + self.plot_tdt = PlotAdis_dt( + canvas=self.canvas_tdt, + results=self._results, + reach_id=self._reach_id, + profile_id=self._profile_id, + pol_id=[0], + key="M", + type_pol=self._type_pol, + trad=self._trad, + toolbar=self.toolbar_tdt + ) + + self.plot_tdt.draw() + + def closeEvent(self, event): + try: + self._timer.stop() + except Exception as e: + logger_exception(e) + + super(ResultsWindowAdisTT, self).closeEvent(event) + + def _compute_status_label(self): + return (self.text_bief() + " | " + + self.text_profile() + " | " + + self.text_time()) + + def text_bief(self): + # Reach + table = self.find(QTableView, f"tableView_reach") + indexes = table.selectedIndexes() + if len(indexes) == 0: + reach = self._study.river.enable_edges()[0] + else: + reach = self._study.river.enable_edges()[indexes[0].row()] + return f"{self._trad['reach']}: {reach.name}" + + def text_profile(self): + # Reach + table = self.find(QTableView, f"tableView_reach") + indexes = table.selectedIndexes() + if len(indexes) == 0: + reach = self._study.river.enable_edges()[0] + else: + reach = self._study.river.enable_edges()[indexes[0].row()] + # Profile + table = self.find(QTableView, f"tableView_profile") + indexes = table.selectedIndexes() + if len(indexes) == 0: + profile = reach.reach.profile(0) + else: + profile = reach.reach.profile(indexes[0].row()) + + pname = profile.name if profile.name != "" else profile.rk + return f"{self._trad['cross_section']}: {pname}" + + def text_time(self): + # Timestamp + ts = self._timestamps[self._slider_time.value()] + + t0 = datetime.fromtimestamp(0) + fts = str( + datetime.fromtimestamp(ts) - t0 + ) + fts = str( + datetime.fromtimestamp(ts) - t0 + ) + fts.replace("days", _translate("Results", "days"))\ + .replace("day", _translate("Results", "day")) + return f"{self._trad['time']} : {fts} ({ts} sec)" + + def text_pollutant(self): + # Pollutant + table = self.find(QTableView, f"tableView_pollutants") + indexes = table.selectedIndexes() + if len(indexes) != 0: + self.pollutant_label = [ + self._results.pollutants_list[i.row()+1] for i in indexes + ] + + return (f"Pollutant: {', '.join(self.pollutant_label)}") + + def setup_statusbar(self): + txt = self._compute_status_label() + self._status_label = QLabel(txt) + self.statusbar.addPermanentWidget(self._status_label) + + def update_statusbar(self): + txt = self._compute_status_label() + self._status_label.setText(txt) + + def setup_connections(self): + # Action + actions = { + "action_reload": self._reload, + # TODO "action_add": self._add_custom_plot, + "action_export": self.export, + } + + self.find(QAction, "action_add").setEnabled(False) + + for action in actions: + self.find(QAction, action).triggered.connect( + actions[action] + ) + + # Table and Plot + fun = { + "reach": self._set_current_reach, + "profile": self._set_current_profile, + "raw_data": self._set_current_profile_raw_data, + } + + for t in ["reach", "profile"]: + table = self.find(QTableView, f"tableView_{t}") + + table.selectionModel()\ + .selectionChanged\ + .connect(fun[t]) + + self._table[t].dataChanged.connect(fun[t]) + + self._slider_time.valueChanged.connect(self._set_current_timestamp) + self._button_play.setChecked(False) + self._button_play.clicked.connect(self._pause) + self._button_back.clicked.connect(self._back) + self._button_next.clicked.connect(self._next) + self._button_first.clicked.connect(self._first) + self._button_last.clicked.connect(self._last) + self._timer.timeout.connect(self._update_slider) + + def update_table_selection_reach(self, ind): + table = self.find(QTableView, f"tableView_reach") + selectionModel = table.selectionModel() + index = table.model().index(ind, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + self._table["profile"].update(ind) + self._table["raw_data"].update(ind) + + def update_table_selection_profile(self, ind): + for t in ["profile"]: + table = self.find(QTableView, f"tableView_{t}") + selectionModel = table.selectionModel() + index = table.model().index(ind, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def update_table_selection_pol(self, ind): + for t in ["pollutants"]: + table = self.find(QTableView, f"tableView_{t}") + selectionModel = table.selectionModel() + index = table.model().index(ind[0] - 1, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def update(self, reach_id=None, profile_id=None, + pol_id=None, timestamp=None): + if reach_id is not None: + self._reach_id = reach_id + self.plot_cdt.set_reach(reach_id) + self.plot_cdx.set_reach(reach_id) + + self.update_table_selection_reach(reach_id) + self.update_table_selection_profile(0) + + if profile_id is not None: + self._profile_id = profile_id + self.plot_cdt.set_profile(profile_id) + self.plot_cdx.set_profile(profile_id) + + self.update_table_selection_profile(profile_id) + + if pol_id is not None: + self._current_pol_id = [p+1 for p in pol_id] # rm total_sediment + self.plot_cdt.set_pollutant(self._current_pol_id) + self.plot_cdx.set_pollutant(self._current_pol_id) + + if timestamp is not None: + self.plot_cdt.set_timestamp(timestamp) + self.plot_cdx.set_timestamp(timestamp) + + self._table["raw_data"].set_timestamp(timestamp) + + self.update_statusbar() + + def _get_current_timestamp(self): + return self._timestamps[ + self._slider_time.value() + ] + + def _set_current_reach(self): + table = self.find(QTableView, f"tableView_reach") + indexes = table.selectedIndexes() + if len(indexes) == 0: + return + + self.update(reach_id=indexes[0].row()) + + def _set_current_profile(self): + table = self.find(QTableView, f"tableView_profile") + indexes = table.selectedIndexes() + if len(indexes) == 0: + return + + self.update(profile_id=indexes[0].row()) + + def _set_current_pol(self): + table = self.find(QTableView, f"tableView_pollutants") + indexes = table.selectedIndexes() + rows = [i.row() for i in indexes] + if len(indexes) == 0: + return + + self.update(pol_id=rows) + + def _set_current_profile_raw_data(self): + return + + def _set_current_timestamp(self): + timestamp = self._timestamps[self._slider_time.value()] + self.update(timestamp=timestamp) + + def _reload_plots(self): + self.plot_cdt.results = self._results + self.plot_cdx.results = self._results + + self.plot_cdt.draw() + self.plot_cdx.draw() + + def _reload_slider(self): + self._slider_time = self.find(QSlider, f"horizontalSlider_time") + self._slider_time.setMaximum(len(self._timestamps) - 1) + self._slider_time.setValue(len(self._timestamps) - 1) + + def _reload(self): + logger.debug("Reload results...") + self._results = self._results.reload() + + self._timestamps = sorted(list(self._results.get("timestamps"))) + + self._reload_plots() + self._reload_slider() + + def _copy(self): + # focus on raw data table + if self.find(QTabWidget, f"tabWidget").currentIndex() == 0: + table = self.find(QTableView, f"tableView_raw_data") + model = self._table["raw_data"] + + coltext = [] + for index in sorted(table.selectionModel().selectedRows()): + row = index.row() + rowtext = [] + for column in range(model.columnCount()): + i = model.createIndex(row, column) + try: + rowtext.append(str(model.data(i))) + except AttributeError: + rowtext.append("") + coltext.append(rowtext) + + self.copyTableIntoClipboard(coltext) + else: + logger.info("TODO: copy") + + def _paste(self): + logger.info("TODO: paste") + + def _undo(self): + self._table.undo() + + def _redo(self): + self._table.redo() + + # play / pause buttons + def _update_slider(self): + if self._slider_time.value() == self._slider_time.maximum(): + self._slider_time.setValue(self._slider_time.minimum()) + else: + self._slider_time.setValue(self._slider_time.value()+1) + + def _next(self): + self._slider_time.setValue(self._slider_time.value()+1) + + def _back(self): + self._slider_time.setValue(self._slider_time.value()-1) + + def _first(self): + self._slider_time.setValue(self._slider_time.minimum()) + + def _last(self): + self._slider_time.setValue(self._slider_time.maximum()) + + def _pause(self): + if self._button_play.isChecked(): + self._button_next.setEnabled(False) + self._button_back.setEnabled(False) + self._button_first.setEnabled(False) + self._button_last.setEnabled(False) + self._timer.start(100) + self._button_play.setIcon(self._icon_pause) + else: + self._timer.stop() + self._button_next.setEnabled(True) + self._button_back.setEnabled(True) + self._button_first.setEnabled(True) + self._button_last.setEnabled(True) + self._button_play.setIcon(self._icon_start) + + def export(self): + + pols = self._results.pollutants_list.copy() + dlg = CustomExportAdisDialog(pollutants=pols, + parent=self) + if dlg.exec(): + x, y, pol = dlg.value + else: + return + + self.file_dialog( + select_file="AnyFile", + callback=lambda f: self.export_to(f[0], x, y, pol), + default_suffix=".csv", + file_filter=["CSV (*.csv)"], + ) + + def export_to(self, filename, x, y, pol): + timestamps = sorted(self._results.get("timestamps")) + reach = self._results.river.reachs[self._reach_id] + first_line = [f"Study: {self._results.study.name}", + f"Reach: {reach.name}"] + if x == "rk": + timestamp = self._get_current_timestamp() + first_line.append(f"Time: {timestamp}s") + val_dict = self._export_rk(timestamp, y, pol) + elif x == "time": + profile = reach.profile(self._profile_id) + pname = profile.name if profile.name != "" else profile.rk + first_line.append(f"Profile: {pname}") + val_dict = self._export_time(self._profile_id, y, pol) + with open(filename, 'w', newline='') as csvfile: + writer = csv.writer(csvfile, delimiter=',', + quotechar='|', quoting=csv.QUOTE_MINIMAL) + dict_x = self._trad.get_dict("values_x") + header = [] + writer.writerow(first_line) + for text in val_dict.keys(): + header.append(text) + writer.writerow(header) + for row in range(len(val_dict[dict_x[x]])): + line = [] + for var in val_dict.keys(): + line.append(val_dict[var][row]) + writer.writerow(line) + + def _export_rk(self, timestamp, y, pol): + reach = self._results.river.reachs[self._reach_id] + dict_x = self._trad.get_dict("values_x") + dict_y = self._trad.get_dict("values_y_pol") + my_dict = {} + my_dict[dict_x["rk"]] = reach.geometry.get_rk() + val_id = {} + val_id["unit_C"] = 0 + val_id["unit_M"] = 2 + val_id["unit_thickness"] = 2 + for unit in y: + if unit == "unit_thickness": + pol_id = 0 + else: + pol_id = self._pol_id_dict[pol] + if unit == "unit_M" and self._type_pol[pol_id] == 1: + my_dict[dict_y[unit]] = [0.0]*len(reach.profiles) + else: + my_dict[dict_y[unit]] = list(map(lambda p: + p.get_ts_key( + timestamp, "pols" + )[pol_id][val_id[unit]], + reach.profiles)) + + return my_dict + + def _export_time(self, profile, y, pol): + reach = self._results.river.reachs[self._reach_id] + profile = reach.profile(profile) + ts = list(self._results.get("timestamps")) + ts.sort() + dict_x = self._trad.get_dict("values_x") + dict_y = self._trad.get_dict("values_y_pol") + my_dict = {} + my_dict[dict_x["time"]] = ts + val_id = {} + val_id["unit_C"] = 0 + val_id["unit_M"] = 2 + val_id["unit_thickness"] = 2 + for unit in y: + if unit == "unit_thickness": + pol_id = 0 + else: + pol_id = self._pol_id_dict[pol] + if unit == "unit_M" and self._type_pol[pol_id] == 1: + my_dict[dict_y[unit]] = [0.0]*len(ts) + else: + my_dict[dict_y[unit]] = list(map(lambda data_el: + data_el[pol_id][val_id[unit]], + profile.get_key("pols") + )) + + return my_dict diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py index 6e6b855e..be240b53 100644 --- a/src/View/Results/translate.py +++ b/src/View/Results/translate.py @@ -121,8 +121,11 @@ class ResultsTranslate(MainTranslate): } tmp = self._sub_dict["table_headers_raw_data"] for pol in self.pollutants: - tmp[pol + " Concentration"] = pol + "\n Concentration" - tmp[pol + " Mass"] = pol + "\n Mass" + if pol == "TEM": + tmp[pol + " Temperature"] = "Temperature (°C)" + else: + tmp[pol + " Concentration"] = pol + "\n Concentration" + tmp[pol + " Mass"] = pol + "\n Mass" else: self._sub_dict["table_headers_raw_data"] = { "name": _translate("Results", "Profile"), @@ -146,6 +149,7 @@ class ResultsTranslate(MainTranslate): self._sub_dict["values_y_pol"] = { "unit_C": self._dict["unit_concentration"], + "unit_T": self._dict["unit_temperature"], "unit_M": self._dict["unit_mass"], "unit_thickness": self._dict["unit_thickness"], } diff --git a/src/View/RunSolver/WindowAdisTS.py b/src/View/RunSolver/WindowAdisTS.py index a768e4d6..b2cafd73 100644 --- a/src/View/RunSolver/WindowAdisTS.py +++ b/src/View/RunSolver/WindowAdisTS.py @@ -83,7 +83,7 @@ class SelectSolverWindowAdisTS(PamhyrDialog): # solvers = self._config.solvers # solvers mage solvers = list(filter( - lambda x: "mage" not in x._type and "rubar" not in x._type, + lambda x: "adists" in x._type.lower(), self._config.solvers )) solvers_name = list( @@ -94,7 +94,8 @@ class SelectSolverWindowAdisTS(PamhyrDialog): ) solvers_mage = list(filter( - lambda x: "mage" or "rubar" in x._type.lower(), + lambda x: ("mage" in x._type.lower() or + "rubar" in x._type.lower()), self._config.solvers )) solvers_mage_names = list(map(lambda x: x._name, solvers_mage)) diff --git a/src/View/RunSolver/WindowAdisTT.py b/src/View/RunSolver/WindowAdisTT.py index 4fc15c82..b2038de1 100644 --- a/src/View/RunSolver/WindowAdisTT.py +++ b/src/View/RunSolver/WindowAdisTT.py @@ -83,7 +83,7 @@ class SelectSolverWindowAdisTT(PamhyrDialog): # solvers = self._config.solvers # solvers mage solvers = list(filter( - lambda x: "mage" not in x._type and "rubar" not in x._type, + lambda x: "adistt" in x._type.lower(), self._config.solvers )) solvers_name = list( @@ -94,7 +94,8 @@ class SelectSolverWindowAdisTT(PamhyrDialog): ) solvers_mage = list(filter( - lambda x: "mage" or "rubar" in x._type.lower(), + lambda x: ("mage" in x._type.lower() or + "rubar" in x._type.lower()), self._config.solvers )) solvers_mage_names = list(map(lambda x: x._name, solvers_mage)) diff --git a/src/View/Tools/Plot/PamhyrCanvas.py b/src/View/Tools/Plot/PamhyrCanvas.py index cebb1d5c..a27505db 100644 --- a/src/View/Tools/Plot/PamhyrCanvas.py +++ b/src/View/Tools/Plot/PamhyrCanvas.py @@ -26,6 +26,7 @@ class MplCanvas(FigureCanvasQTAgg): layout='constrained', ) super(MplCanvas, self).__init__(fig) + self.setMinimumSize(1, 1) self.axes = fig.add_subplot(111) self.axes.format_coord = lambda x, y: ( @@ -38,6 +39,13 @@ class MplCanvas(FigureCanvasQTAgg): self.axes.spines[['top', 'right']].set_color('none') self.add_arrows() + def resizeEvent(self, event): + """Ignore transient invalid sizes produced by hidden Qt tabs.""" + if self.width() <= 0 or self.height() <= 0: + return + + super(MplCanvas, self).resizeEvent(event) + def add_arrows(self): al = 8. arrowprops = dict( diff --git a/src/View/ui/ResultsAdisTT.ui b/src/View/ui/ResultsAdisTT.ui new file mode 100644 index 00000000..69dd6188 --- /dev/null +++ b/src/View/ui/ResultsAdisTT.ui @@ -0,0 +1,265 @@ + + + MainWindow + + + + 0 + 0 + 1410 + 821 + + + + MainWindow + + + + + + + + 874 + 0 + + + + + + + Qt::Horizontal + + + + Qt::Vertical + + + + + + + + + + + + + + + + + + + + + + ressources/media-skip-backward.pngressources/media-skip-backward.png + + + + + + + + + + + ressources/media-seek-backward.pngressources/media-seek-backward.png + + + + + + + + + + + ressources/player_play.png + ressources/player_pause.pngressources/player_play.png + + + true + + + true + + + + + + + + + + + ressources/media-seek-forward.pngressources/media-seek-forward.png + + + + + + + + + + + ressources/media-skip-forward.pngressources/media-skip-forward.png + + + + + + + Qt::Horizontal + + + + + + + + + true + + + 1 + + + + Raw data + + + + + + + + + + + + + + + + + true + + + Temperature + + + + + + + + 0 + + + + Temperature dx + + + + + + + + + + Temperature dt + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1410 + 20 + + + + + + + true + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + ressources/add.pngressources/add.png + + + Add + + + Add custom visualization + + + + + + ressources/reload.pngressources/reload.png + + + Reload + + + + + + ressources/export.pngressources/export.png + + + Export + + + Export raw data + + + Ctrl+E + + + + + + + + + +