diff --git a/src/Model/BoundaryConditionsTemperature/BoundaryConditionTemperature.py b/src/Model/BoundaryConditionsTemperature/BoundaryConditionTemperature.py index c5b773f4..31b0d7f2 100644 --- a/src/Model/BoundaryConditionsTemperature/BoundaryConditionTemperature.py +++ b/src/Model/BoundaryConditionsTemperature/BoundaryConditionTemperature.py @@ -77,7 +77,7 @@ class Data(SQLSubModel): created = True if major == "0" and (int(minor) < 2 or - (int(minor) == 2 and int(release) < 7)): + (int(minor) == 2 and int(release) <= 7)): if not cls.is_table_exists(execute, "boundary_condition_data_temperature"): cls._db_create(execute) @@ -205,7 +205,7 @@ class BoundaryConditionTemperature(SQLSubModel): created = False if major == "0" and (int(minor) < 2 or ( - int(minor) == 2 and int(release) < 7)): + int(minor) == 2 and int(release) <= 7)): if not cls.is_table_exists(execute, "boundary_condition_temperature"): cls._db_create(execute) diff --git a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSSpec.py b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSSpec.py index 89277bee..3ed5e3dc 100644 --- a/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSSpec.py +++ b/src/Model/InitialConditionsAdisTS/InitialConditionsAdisTSSpec.py @@ -192,7 +192,7 @@ class ICAdisTSSpec(SQLSubModel): new_spec.rate = rate new_spec.enabled = enabled - # loaded.add(pid) + loaded.add(id) new.append(new_spec) data["scenario"] = scenario.parent @@ -237,6 +237,7 @@ class ICAdisTSSpec(SQLSubModel): def name(self, name): self._name_section = name self.modified() + self.modified() @property def reach(self): @@ -246,6 +247,7 @@ class ICAdisTSSpec(SQLSubModel): def reach(self, reach): self._reach = reach self.modified() + self.modified() @property def start_rk(self): @@ -255,6 +257,7 @@ class ICAdisTSSpec(SQLSubModel): def start_rk(self, start_rk): self._start_rk = start_rk self.modified() + self.modified() @property def end_rk(self): @@ -264,6 +267,7 @@ class ICAdisTSSpec(SQLSubModel): def end_rk(self, end_rk): self._end_rk = end_rk self.modified() + self.modified() @property def concentration(self): @@ -273,6 +277,7 @@ class ICAdisTSSpec(SQLSubModel): def concentration(self, concentration): self._concentration = concentration self.modified() + self.modified() @property def eg(self): @@ -282,6 +287,7 @@ class ICAdisTSSpec(SQLSubModel): def eg(self, eg): self._eg = eg self.modified() + self.modified() @property def em(self): @@ -291,6 +297,7 @@ class ICAdisTSSpec(SQLSubModel): def em(self, em): self._em = em self.modified() + self.modified() @property def ed(self): @@ -300,6 +307,7 @@ class ICAdisTSSpec(SQLSubModel): def ed(self, ed): self._ed = ed self.modified() + self.modified() @property def rate(self): @@ -309,6 +317,7 @@ class ICAdisTSSpec(SQLSubModel): def rate(self, rate): self._rate = rate self.modified() + self.modified() @property def enabled(self): diff --git a/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py b/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py index 986e603e..4f5885fd 100644 --- a/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py +++ b/src/Model/InitialConditionsTemperature/InitialConditionsTemperature.py @@ -68,7 +68,7 @@ class InitialConditionsTemperature(SQLSubModel): major, minor, release = version.strip().split(".") if major == "0": - if int(minor) < 2 or (int(minor) == 2 and int(release) < 7): + if int(minor) < 2 or (int(minor) == 2 and int(release) <= 7): table_name = "initial_conditions_temperature" if not cls.is_table_exists(execute, table_name): cls._db_create(execute) diff --git a/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureSpec.py b/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureSpec.py index 431093fc..48647b17 100644 --- a/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureSpec.py +++ b/src/Model/InitialConditionsTemperature/InitialConditionsTemperatureSpec.py @@ -31,10 +31,11 @@ class ICTemperatureSpec(SQLSubModel): _sub_classes = [] def __init__(self, id: int = -1, name: str = "", - status=None, owner_scenario=None): - super(ICTemperatureSpec, self).__init__() - - self._status = status + status=None, owner_scenario=-1): + super(ICTemperatureSpec, self).__init__( + id=id, status=status, + owner_scenario=owner_scenario + ) self._name_section = name self._reach = None @@ -69,7 +70,7 @@ class ICTemperatureSpec(SQLSubModel): major, minor, release = version.strip().split(".") if major == "0": - if int(minor) < 2 or (int(minor) == 2 and int(release) < 7): + if int(minor) < 2 or (int(minor) == 2 and int(release) <= 7): table_name = "initial_conditions_temperature_spec" if not cls.is_table_exists(execute, table_name): cls._db_create(execute) @@ -122,7 +123,7 @@ class ICTemperatureSpec(SQLSubModel): new_spec.end_rk = end_rk new_spec.temperature = temperature - # loaded.add(pid) + loaded.add(id) new.append(new_spec) data["scenario"] = scenario.parent @@ -163,7 +164,7 @@ class ICTemperatureSpec(SQLSubModel): @name.setter def name(self, name): self._name_section = name - self._status.modified() + self.modified() @property def reach(self): @@ -172,7 +173,7 @@ class ICTemperatureSpec(SQLSubModel): @reach.setter def reach(self, reach): self._reach = reach - self._status.modified() + self.modified() @property def start_rk(self): @@ -181,7 +182,7 @@ class ICTemperatureSpec(SQLSubModel): @start_rk.setter def start_rk(self, start_rk): self._start_rk = start_rk - self._status.modified() + self.modified() @property def end_rk(self): @@ -190,7 +191,7 @@ class ICTemperatureSpec(SQLSubModel): @end_rk.setter def end_rk(self, end_rk): self._end_rk = end_rk - self._status.modified() + self.modified() @property def temperature(self): diff --git a/src/Model/Study.py b/src/Model/Study.py index ded212ba..8b80efb8 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -46,7 +46,7 @@ logger = logging.getLogger() class Study(SQLModel): - _version = "0.2.7" + _version = "0.2.8" _sub_classes = [ Scenario, diff --git a/src/Model/WeatherParameters/WeatherParameters.py b/src/Model/WeatherParameters/WeatherParameters.py index a440173a..b532e885 100644 --- a/src/Model/WeatherParameters/WeatherParameters.py +++ b/src/Model/WeatherParameters/WeatherParameters.py @@ -73,7 +73,7 @@ class Data(SQLSubModel): major, minor, release = version.strip().split(".") if major == "0" and (int(minor) < 2 or - (int(minor) == 2 and int(release) < 7)): + (int(minor) == 2 and int(release) <= 7)): if not cls.is_table_exists(execute, "air_temperature_data"): cls._db_create(execute) @@ -341,7 +341,7 @@ class WeatherParameters(SQLSubModel): created = False if major == "0" and (int(minor) < 2 or - (int(minor) == 2 and int(release) < 7)): + (int(minor) == 2 and int(release) <= 7)): if not cls.is_table_exists(execute, "air_temperature"): cls._db_create(execute) created = True @@ -652,14 +652,38 @@ class WeatherParameters(SQLSubModel): def add(self, index: int): value = Data(self._default_0, self._default_1, status=self._status) - self._data.insert(index, value) - self.modified() + self.insert(index, value) return value def insert(self, index: int, value): + visible_data = self.data + if index < len(visible_data): + index = self._data.index(visible_data[index]) + else: + index = len(self._data) self._data.insert(index, value) self.modified() + def set_data_value(self, value, column, data): + value[column] = self._types[column](data) + self.modified() + + def set_deleted(self, values, deleted): + for value in values: + if deleted: + value.set_as_deleted() + else: + value.set_as_not_deleted() + self.modified() + + def reorder(self, values): + values = iter(values) + self._data = [ + value if value.is_deleted() else next(values) + for value in self._data + ] + self.modified() + def delete_i(self, indexes): self._data = list( map( @@ -708,10 +732,7 @@ class WeatherParameters(SQLSubModel): 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() + self.set_data_value(self.get_i(index), column, value) def set_i_0(self, index: int, value): self._set_i_c_v(index, 0, value) @@ -742,15 +763,13 @@ class WeatherParameters(SQLSubModel): return new def move_up(self, index): - if index < len(self): - next = index - 1 - d = self._data - d[index], d[next] = d[next], d[index] - self.modified() + if 0 < index < len(self): + data = self.data + data[index - 1], data[index] = data[index], data[index - 1] + self.reorder(data) def move_down(self, index): - if index >= 0: - prev = index + 1 - d = self._data - d[index], d[prev] = d[prev], d[index] - self.modified() + if 0 <= index < len(self) - 1: + data = self.data + data[index], data[index + 1] = data[index + 1], data[index] + self.reorder(data) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 0827a094..645a7c07 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -102,6 +102,7 @@ class GeometryWindow(PamhyrWindow): self.setup_plots() self.setup_statusbar() self.setup_connections() + self.update_meshing_action() def setup_table(self): if self._study.is_read_only(): @@ -231,7 +232,16 @@ class GeometryWindow(PamhyrWindow): def update_redraw(self): self._update(redraw=True) + def update_meshing_action(self): + enabled = ( + not self._study.is_read_only() + and self._reach.number_profiles > 0 + ) + self.find(QAction, "action_meshing").setEnabled(enabled) + def _update(self, redraw=False, propagate=True): + self.update_meshing_action() + if redraw: self._plot_xy.redraw(data=self._reach) self._plot_rkc.redraw(data=self._reach) @@ -302,6 +312,9 @@ class GeometryWindow(PamhyrWindow): self.tableView.model().blockSignals(False) def edit_meshing(self): + if self._reach.number_profiles == 0: + return + rows = list( set( (i.row() for i in self.tableView.selectedIndexes()) diff --git a/src/View/InitialConditionsAdisTS/Table.py b/src/View/InitialConditionsAdisTS/Table.py index 83864d57..08fcfdac 100644 --- a/src/View/InitialConditionsAdisTS/Table.py +++ b/src/View/InitialConditionsAdisTS/Table.py @@ -205,13 +205,14 @@ class InitialConditionTableModel(PamhyrTableModel): if self._headers[column] != "reach": self._undo.push( SetCommandSpec( - self._lst, row, self._headers[column], value + self._data, self._lst, row, + self._headers[column], value ) ) elif self._headers[column] == "reach": self._undo.push( SetCommandSpec( - self._lst, row, self._headers[column], + self._data, self._lst, row, self._headers[column], self._river.edge(value).id ) ) diff --git a/src/View/InitialConditionsAdisTS/UndoCommand.py b/src/View/InitialConditionsAdisTS/UndoCommand.py index d5defc0a..af17232e 100644 --- a/src/View/InitialConditionsAdisTS/UndoCommand.py +++ b/src/View/InitialConditionsAdisTS/UndoCommand.py @@ -80,31 +80,31 @@ class SetCommand(QUndoCommand): class SetCommandSpec(QUndoCommand): - def __init__(self, data, row, column, new_value): + def __init__(self, parent, data, row, column, new_value): QUndoCommand.__init__(self) - self._data = data - self._row = row + self._parent = parent + self._value = data[row] self._column = column if self._column == "name": - self._old = self._data[self._row].name + self._old = self._value.name elif self._column == "reach": - self._old = self._data[self._row].reach + self._old = self._value.reach elif self._column == "start_rk": - self._old = self._data[self._row].start_rk + self._old = self._value.start_rk elif self._column == "end_rk": - self._old = self._data[self._row].end_rk + self._old = self._value.end_rk elif self._column == "concentration": - self._old = self._data[self._row].concentration + self._old = self._value.concentration elif self._column == "eg": - self._old = self._data[self._row].eg + self._old = self._value.eg elif self._column == "em": - self._old = self._data[self._row].em + self._old = self._value.em elif self._column == "ed": - self._old = self._data[self._row].ed + self._old = self._value.ed elif self._column == "rate": - self._old = self._data[self._row].rate + self._old = self._value.rate _type = float if column == "name": @@ -115,44 +115,12 @@ class SetCommandSpec(QUndoCommand): self._new = _type(new_value) def undo(self): - if self._column == "name": - self._data[self._row].name = self._old - elif self._column == "reach": - self._data[self._row].reach = self._old - elif self._column == "start_rk": - self._data[self._row].start_rk = self._old - elif self._column == "end_rk": - self._data[self._row].end_rk = self._old - elif self._column == "concentration": - self._data[self._row].concentration = self._old - elif self._column == "eg": - self._data[self._row].eg = self._old - elif self._column == "em": - self._data[self._row].em = self._old - elif self._column == "ed": - self._data[self._row].ed = self._old - elif self._column == "rate": - self._data[self._row].rate = self._old + setattr(self._value, self._column, self._old) + self._parent.modified() def redo(self): - if self._column == "name": - self._data[self._row].name = self._new - elif self._column == "reach": - self._data[self._row].reach = self._new - elif self._column == "start_rk": - self._data[self._row].start_rk = self._new - elif self._column == "end_rk": - self._data[self._row].end_rk = self._new - elif self._column == "concentration": - self._data[self._row].concentration = self._new - elif self._column == "eg": - self._data[self._row].eg = self._new - elif self._column == "em": - self._data[self._row].em = self._new - elif self._column == "ed": - self._data[self._row].ed = self._new - elif self._column == "rate": - self._data[self._row].rate = self._new + setattr(self._value, self._column, self._new) + self._parent.modified() class AddCommand(QUndoCommand): diff --git a/src/View/InitialConditionsTemperature/Table.py b/src/View/InitialConditionsTemperature/Table.py index c2a6fca8..0de49aef 100644 --- a/src/View/InitialConditionsTemperature/Table.py +++ b/src/View/InitialConditionsTemperature/Table.py @@ -30,13 +30,13 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QPushButton, QLineEdit, QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, - QComboBox, + QComboBox, QMessageBox, ) from View.Tools.PamhyrTable import PamhyrTableModel from View.InitialConditionsTemperature.UndoCommand import ( - SetCommand, AddCommand, SetCommandSpec, + AddCommand, SetCommandSpec, DelCommand, ) @@ -229,13 +229,22 @@ class InitialConditionTableModel(PamhyrTableModel): row = index.row() column = index.column() + column_name = self._headers[column] try: - if self._headers[column] in ["name", "temperature"]: - self._undo.push( - SetCommand( - self._lst, row, self._headers[column], value - ) + new_value = ( + self._river.edge(value).id + if column_name == "reach" + else value + ) + if (column_name in ("reach", "rk") + and self._overlaps_existing_rk( + row, column_name, new_value + )): + QMessageBox.warning( + self._table_view, + self._trad["msg_rk_overlap_title"], + self._trad["msg_rk_overlap_text"] ) else: new_value = value @@ -265,6 +274,25 @@ class InitialConditionTableModel(PamhyrTableModel): self.dataChanged.emit(index, index) return True + def _overlaps_existing_rk(self, row, column, value): + current = self._lst[row] + reach = value if column == "reach" else current.reach + rk = value if column == "rk" else current.start_rk + if reach in (None, -1) or rk is None: + return False + + rk = float(rk) + return any( + other is not current + and not other.is_deleted() + and other.reach == reach + and other.start_rk is not None + and other.end_rk is not None + and min(other.start_rk, other.end_rk) <= rk + <= max(other.start_rk, other.end_rk) + for other in self._data._data + ) + def add(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) diff --git a/src/View/InitialConditionsTemperature/UndoCommand.py b/src/View/InitialConditionsTemperature/UndoCommand.py index 12de80a1..b7746f64 100644 --- a/src/View/InitialConditionsTemperature/UndoCommand.py +++ b/src/View/InitialConditionsTemperature/UndoCommand.py @@ -62,23 +62,23 @@ class SetCommand(QUndoCommand): class SetCommandSpec(QUndoCommand): - def __init__(self, data, row, column, new_value): + def __init__(self, parent, data, row, column, new_value): QUndoCommand.__init__(self) - self._data = data - self._row = row + self._parent = parent + self._value = data[row] self._column = column if self._column == "name": - self._old = self._data[self._row].name + self._old = self._value.name elif self._column == "reach": self._old = self._data[self._row].reach self._old_start_rk = self._data[self._row].start_rk self._old_end_rk = self._data[self._row].end_rk elif self._column == "rk": - self._old = self._data[self._row].start_rk + self._old = self._value.start_rk elif self._column == "temperature": - self._old = self._data[self._row].temperature + self._old = self._value.temperature _type = float if column == "name": @@ -94,29 +94,31 @@ class SetCommandSpec(QUndoCommand): def undo(self): if self._column == "name": - self._data[self._row].name = self._old + self._value.name = self._old elif self._column == "reach": self._data[self._row].reach = self._old self._data[self._row].start_rk = self._old_start_rk self._data[self._row].end_rk = self._old_end_rk elif self._column == "rk": - self._data[self._row].start_rk = self._old - self._data[self._row].end_rk = self._old + self._value.start_rk = self._old + self._value.end_rk = self._old elif self._column == "temperature": - self._data[self._row].temperature = self._old + self._value.temperature = self._old + self._parent.modified() def redo(self): if self._column == "name": - self._data[self._row].name = self._new + self._value.name = self._new elif self._column == "reach": self._data[self._row].reach = self._new self._data[self._row].start_rk = self._new_rk self._data[self._row].end_rk = self._new_rk elif self._column == "rk": - self._data[self._row].start_rk = self._new - self._data[self._row].end_rk = self._new + self._value.start_rk = self._new + self._value.end_rk = self._new elif self._column == "temperature": - self._data[self._row].temperature = self._new + self._value.temperature = self._new + self._parent.modified() class AddCommand(QUndoCommand): diff --git a/src/View/InitialConditionsTemperature/translate.py b/src/View/InitialConditionsTemperature/translate.py index d8517008..b856b7cd 100644 --- a/src/View/InitialConditionsTemperature/translate.py +++ b/src/View/InitialConditionsTemperature/translate.py @@ -32,6 +32,16 @@ class IcTemperatureTranslate(MainTranslate): self._dict["rk"] = self._dict["unit_rk"] + self._dict["msg_rk_overlap_title"] = _translate( + "InitialConditionTemperature", + "Chainage already used" + ) + self._dict["msg_rk_overlap_text"] = _translate( + "InitialConditionTemperature", + "An initial temperature condition is already defined at this " + "chainage on the selected reach." + ) + self._sub_dict["table_headers"] = { "name": self._dict["name"], "temperature": self._dict["unit_temperature"], diff --git a/src/View/Results/PlotTemperature.py b/src/View/Results/PlotTemperature.py new file mode 100644 index 00000000..b9ef76e0 --- /dev/null +++ b/src/View/Results/PlotTemperature.py @@ -0,0 +1,215 @@ +# PlotTemperature.py -- Pamhyr +# Copyright (C) 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. + +# -*- coding: utf-8 -*- + +import numpy as np + +from matplotlib.cm import ScalarMappable +from matplotlib.collections import PolyCollection +from matplotlib.colors import Normalize + +from View.Results.PlotXY import PlotXY + + +class PlotTemperature(PlotXY): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + pol_id=1, parent=None): + super(PlotTemperature, self).__init__( + canvas=canvas, + trad=trad, + toolbar=toolbar, + results=results, + reach_id=reach_id, + profile_id=profile_id, + res_id=[0], + parent=parent, + ) + + self._current_pol_id = pol_id + self._global_ranges = {} + self._temperature_zones = None + self._colorbar = None + self._auto_relim_update = False + self._autoscale_update = False + + @property + def results(self): + return self.data + + @results.setter + def results(self, results): + self.data = results + self._timestamps = sorted(results.get("timestamps")) + self._current_timestamp = self._timestamps[-1] + self._global_ranges.clear() + + def draw(self, highlight=None): + if self._colorbar is not None: + self._colorbar.remove() + self._colorbar = None + self.init_axes() + + reach = self.results.river.reach(self._current_reach_id) + if reach.geometry.number_profiles == 0: + self._init = False + return + + temperatures = self._temperatures(reach) + norm = self._temperature_norm() + + self.draw_profiles(reach, self.results.river.reachs) + self.draw_guide_lines(reach) + self._draw_temperature_zones(reach, temperatures, norm) + self.draw_current(reach) + + mappable = self._temperature_zones + if mappable is None: + mappable = ScalarMappable(norm=norm, cmap="coolwarm") + self._colorbar = self.canvas.figure.colorbar( + mappable, ax=self.canvas.axes + ) + self._colorbar.set_label(self._trad["unit_temperature"]) + # self.canvas.axes.set_title( + # f"{self._trad['temperature_map']} — {reach.name}" + # ) + self.canvas.axes.set_aspect("auto") + self._zoom_to_reach_bbox(reach) + self.canvas.draw_idle() + self.toolbar_update() + self._init = True + + def _draw_temperature_zones(self, reach, temperatures, norm): + profiles = reach.profiles + polygons = [] + for index in range(len(profiles) - 1): + current = profiles[index].geometry + following = profiles[index + 1].geometry + if current.number_points == 0 or following.number_points == 0: + continue + polygons.append([ + (current.x()[0], current.y()[0]), + (current.x()[-1], current.y()[-1]), + (following.x()[-1], following.y()[-1]), + (following.x()[0], following.y()[0]), + ]) + + if not polygons: + self._temperature_zones = None + return + + self._temperature_zones = PolyCollection( + polygons, + cmap="coolwarm", + norm=norm, + edgecolors="none", + alpha=0.8, + zorder=1, + ) + self._temperature_zones.set_array( + self._segment_temperatures(temperatures) + ) + self.canvas.axes.add_collection(self._temperature_zones) + + def update(self): + if not self._init or self._temperature_zones is None: + self.draw() + return + + reach = self.results.river.reach(self._current_reach_id) + self._temperature_zones.set_array( + self._segment_temperatures(self._temperatures(reach)) + ) + self.canvas.draw_idle() + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + reach = self.results.river.reach(self._current_reach_id) + profile = reach.profile(profile_id) + self.plot_selected.set_data( + profile.geometry.x(), profile.geometry.y() + ) + self.canvas.draw_idle() + + def set_pollutant(self, pol_id): + self._current_pol_id = pol_id + self.draw() + + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.update() + + def _temperatures(self, reach): + return np.asarray([ + profile.get_ts_key(self._current_timestamp, "pols")[ + self._current_pol_id + ][0] + for profile in reach.profiles + ], dtype=float) + + @staticmethod + def _segment_temperatures(temperatures): + return (temperatures[:-1] + temperatures[1:]) / 2.0 + + def _temperature_norm(self): + minimum, maximum = self._global_temperature_range() + if minimum == maximum: + maximum = minimum + 1.0 + return Normalize(vmin=minimum, vmax=maximum) + + def _zoom_to_reach_bbox(self, reach): + x_values = [ + np.asarray(profile.geometry.x(), dtype=float) + for profile in reach.profiles + if len(profile.geometry.x()) != 0 + ] + y_values = [ + np.asarray(profile.geometry.y(), dtype=float) + for profile in reach.profiles + if len(profile.geometry.y()) != 0 + ] + if not x_values or not y_values: + return + x = np.concatenate(x_values) + y = np.concatenate(y_values) + + x_min, x_max = float(np.min(x)), float(np.max(x)) + y_min, y_max = float(np.min(y)), float(np.max(y)) + x_margin = max((x_max - x_min) * 0.05, 1.0) + y_margin = max((y_max - y_min) * 0.05, 1.0) + self.canvas.axes.set_xlim(x_min - x_margin, x_max + x_margin) + self.canvas.axes.set_ylim(y_min - y_margin, y_max + y_margin) + + def _global_temperature_range(self): + pol_id = self._current_pol_id + if pol_id in self._global_ranges: + return self._global_ranges[pol_id] + + temperatures = [] + for reach in self.results.river.reachs: + for profile in reach.profiles: + for timestamp in self._timestamps: + values = profile.get_ts_key(timestamp, "pols") + if values is not None: + temperatures.append(values[pol_id][0]) + + temperatures = np.asarray(temperatures, dtype=float) + temperatures = temperatures[np.isfinite(temperatures)] + value_range = ( + (float(np.min(temperatures)), float(np.max(temperatures))) + if temperatures.size + else (0.0, 1.0) + ) + self._global_ranges[pol_id] = value_range + return value_range diff --git a/src/View/Results/WindowAdisTT.py b/src/View/Results/WindowAdisTT.py index 7ef9a5c5..cbe7b1d4 100644 --- a/src/View/Results/WindowAdisTT.py +++ b/src/View/Results/WindowAdisTT.py @@ -47,6 +47,7 @@ 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.PlotTemperature import PlotTemperature from View.Results.CustomPlot.Plot import CustomPlot from View.Results.CustomExport.CustomExportAdis import ( @@ -225,6 +226,33 @@ class ResultsWindowAdisTT(PamhyrWindow): ) self.plot_cdx.draw() + self.canvas_temperature_map = MplCanvas(width=5, height=4, dpi=100) + self.canvas_temperature_map.setObjectName("canvas_temperature_map") + self.toolbar_temperature_map = PamhyrPlotToolbar( + self.canvas_temperature_map, self, items=[ + "home", "move", "zoom", "save", "iso", "back/forward" + ] + ) + temperature_map_tab = QWidget() + temperature_map_layout = QVBoxLayout(temperature_map_tab) + temperature_map_layout.addWidget(self.toolbar_temperature_map) + temperature_map_layout.addWidget(self.canvas_temperature_map) + self.find(QTabWidget, "tabWidget_c").addTab( + temperature_map_tab, + self._trad["temperature_map"] + ) + self.plot_temperature = PlotTemperature( + canvas=self.canvas_temperature_map, + results=self._results, + reach_id=self._reach_id, + profile_id=self._profile_id, + pol_id=self._current_pol_id[0], + trad=self._trad, + toolbar=self.toolbar_temperature_map, + parent=self, + ) + self.plot_temperature.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. @@ -507,6 +535,7 @@ class ResultsWindowAdisTT(PamhyrWindow): self._reach_id = reach_id self.plot_cdt.set_reach(reach_id) self.plot_cdx.set_reach(reach_id) + self.plot_temperature.set_reach(reach_id) self.update_table_selection_reach(reach_id) self.update_table_selection_profile(0) @@ -515,6 +544,7 @@ class ResultsWindowAdisTT(PamhyrWindow): self._profile_id = profile_id self.plot_cdt.set_profile(profile_id) self.plot_cdx.set_profile(profile_id) + self.plot_temperature.set_profile(profile_id) self.update_table_selection_profile(profile_id) @@ -522,10 +552,12 @@ class ResultsWindowAdisTT(PamhyrWindow): 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) + self.plot_temperature.set_pollutant(self._current_pol_id[0]) if timestamp is not None: self.plot_cdt.set_timestamp_preserve_view(timestamp) self.plot_cdx.set_timestamp_preserve_view(timestamp) + self.plot_temperature.set_timestamp(timestamp) self._table["raw_data"].set_timestamp(timestamp) @@ -574,6 +606,8 @@ class ResultsWindowAdisTT(PamhyrWindow): self.plot_cdt.draw() self.plot_cdx.draw() + self.plot_temperature.results = self._results + self.plot_temperature.draw() def _reload_slider(self): self._slider_time = self.find(QSlider, f"horizontalSlider_time") diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py index be240b53..d4b103d8 100644 --- a/src/View/Results/translate.py +++ b/src/View/Results/translate.py @@ -46,6 +46,12 @@ class ResultsTranslate(MainTranslate): self._dict['solver'] = _translate("Results", "Solver") self._dict['x'] = _translate("Results", "X (m)") + self._dict["temperature_map"] = _translate( + "Results", "Reach temperature map" + ) + self._dict["temperature"] = _translate( + "Results", "Temperature" + ) self._dict['label_bottom'] = _translate("Results", "Bottom") self._dict['label_water'] = _translate("Results", "Water elevation") diff --git a/src/View/WeatherParameters/Edit/Table.py b/src/View/WeatherParameters/Edit/Table.py index 996ef4b7..a77b0e2d 100644 --- a/src/View/WeatherParameters/Edit/Table.py +++ b/src/View/WeatherParameters/Edit/Table.py @@ -145,35 +145,24 @@ class TableModel(PamhyrTableModel): if row <= 0: return - target = row + 2 - - self.beginMoveRows(parent, row - 1, row - 1, parent, target) - - self._undo_stack.push( + self.layoutAboutToBeChanged.emit() + self._undo.push( MoveCommand( self._data, "up", row ) ) - - self.endMoveRows() self.update() - def move_down(self, index, parent=QModelIndex()): - row = index.row() - if row >= len(self._data): + def move_down(self, row, parent=QModelIndex()): + if row < 0 or row >= len(self._data) - 1: return - target = row - - self.beginMoveRows(parent, row + 1, row + 1, parent, target) - - self._undo_stack.push( + self.layoutAboutToBeChanged.emit() + self._undo.push( MoveCommand( self._data, "down", row ) ) - - self.endMoveRows() self.update() def paste(self, row, header, data): diff --git a/src/View/WeatherParameters/Edit/UndoCommand.py b/src/View/WeatherParameters/Edit/UndoCommand.py index f18eab7f..a8164fac 100644 --- a/src/View/WeatherParameters/Edit/UndoCommand.py +++ b/src/View/WeatherParameters/Edit/UndoCommand.py @@ -31,17 +31,17 @@ class SetDataCommand(QUndoCommand): QUndoCommand.__init__(self) self._data = data - self._index = index + self._value = self._data.get_i(index) self._column = column - self._old = self._data.get_i(self._index)[self._column] + self._old = self._value[self._column] _type = self._data.get_type_column(self._column) self._new = _type(new_value) def undo(self): - self._data._set_i_c_v(self._index, self._column, self._old) + self._data.set_data_value(self._value, self._column, self._old) def redo(self): - self._data._set_i_c_v(self._index, self._column, self._new) + self._data.set_data_value(self._value, self._column, self._new) class AddCommand(QUndoCommand): @@ -53,13 +53,13 @@ class AddCommand(QUndoCommand): self._new = None def undo(self): - self._data.delete_i([self._index]) + self._data.set_deleted([self._new], True) def redo(self): if self._new is None: self._new = self._data.add(self._index) else: - self._data.insert(self._index, self._new) + self._data.set_deleted([self._new], False) class DelCommand(QUndoCommand): @@ -75,10 +75,10 @@ class DelCommand(QUndoCommand): self._wp.sort() def undo(self): - self._data.set_as_not_deleted_i(self._rows) + self._data.set_deleted([wp for row, wp in self._wp], False) def redo(self): - self._data.set_as_deleted_i(self._rows) + self._data.set_deleted([wp for row, wp in self._wp], True) class SortCommand(QUndoCommand): @@ -89,27 +89,17 @@ class SortCommand(QUndoCommand): self._reverse = _reverse self._old = self._data.data - self._indexes = None + self._new = sorted( + self._old, + key=lambda value: value[0], + reverse=self._reverse + ) def undo(self): - ll = self._data.data - self._data.sort( - key=lambda x: self._indexes[ll.index(x)] - ) + self._data.reorder(self._old) 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 + self._data.reorder(self._new) class MoveCommand(QUndoCommand): @@ -117,20 +107,16 @@ class MoveCommand(QUndoCommand): QUndoCommand.__init__(self) self._data = data - self._up = up == "up" - self._i = i + self._old = self._data.data + self._new = self._old.copy() + other = i - 1 if up == "up" else i + 1 + self._new[i], self._new[other] = self._new[other], self._new[i] def undo(self): - if self._up: - self._data.move_up(self._i) - else: - self._data.move_down(self._i) + self._data.reorder(self._old) def redo(self): - if self._up: - self._data.move_up(self._i) - else: - self._data.move_down(self._i) + self._data.reorder(self._new) class PasteCommand(QUndoCommand): @@ -141,15 +127,18 @@ class PasteCommand(QUndoCommand): self._row = row self._wps = list(wps) self._wps.reverse() + self._inserted = False def undo(self): - self._data.delete_i( - range(self._row, self._row + len(self._wps)) - ) + self._data.set_deleted(self._wps, True) def redo(self): - for wp in self._wps: - self._data.insert(self._row, wp) + if not self._inserted: + for wp in self._wps: + self._data.insert(self._row, wp) + self._inserted = True + else: + self._data.set_deleted(self._wps, False) class DuplicateCommand(QUndoCommand): @@ -160,10 +149,15 @@ class DuplicateCommand(QUndoCommand): self._rows = rows self._wp = deepcopy(bc) self._wp.reverse() + self._inserted = False def undo(self): - self._data.delete(self._wp) + self._data.set_deleted(self._wp, True) def redo(self): - for bc in self._wp: - self._data.insert(self._rows[0], bc) + if not self._inserted: + for bc in self._wp: + self._data.insert(self._rows[0], bc) + self._inserted = True + else: + self._data.set_deleted(self._wp, False) diff --git a/src/View/WeatherParameters/Table.py b/src/View/WeatherParameters/Table.py index b3e05398..9271481b 100644 --- a/src/View/WeatherParameters/Table.py +++ b/src/View/WeatherParameters/Table.py @@ -30,7 +30,7 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QPushButton, QLineEdit, QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, - QComboBox, + QComboBox, QMessageBox, ) from View.Tools.PamhyrTable import PamhyrTableModel @@ -245,6 +245,10 @@ class WeatherParametersTableModel(PamhyrTableModel): p for p in _edge.reach.profiles if p.pamhyr_id == value ) + if self._overlaps_existing_interval( + row, begin_section=_begin_rk): + self._show_overlap_warning() + return False self._undo.push( SetBeginCommand( self._data, global_row, _begin_rk @@ -256,18 +260,32 @@ class WeatherParametersTableModel(PamhyrTableModel): p for p in _edge.reach.profiles if p.pamhyr_id == value ) + if self._overlaps_existing_interval( + row, end_section=_end_rk): + self._show_overlap_warning() + return False self._undo.push( SetEndCommand( self._data, global_row, _end_rk ) ) elif self._headers[column] == "reach": + new_reach = self._river.edge(value) + overlaps = self._overlaps_existing_interval( + row, reach=new_reach + ) + current = self._lst[row] + clear_interval = overlaps and current.reach is None + if overlaps and not clear_interval: + self._show_overlap_warning() + return False edge = self._river.edge(value) if not edge.reach.profiles: return False self._undo.push( SetEdgeCommand( - self._data, global_row, edge + self._data, global_row, new_reach, + clear_interval=clear_interval ) ) except Exception as e: @@ -283,6 +301,48 @@ class WeatherParametersTableModel(PamhyrTableModel): self.dataChanged.emit(index, index) return True + def _overlaps_existing_interval(self, row, reach=None, + begin_section=None, end_section=None): + current = self._lst[row] + reach = current.reach if reach is None else reach + if reach is None: + return False + + if begin_section is None: + begin_section = ( + reach.reach.profiles[0] + if reach is not current.reach and reach.reach.profiles + else current.begin_section + ) + if end_section is None: + end_section = ( + reach.reach.profiles[-1] + if reach is not current.reach and reach.reach.profiles + else current.end_section + ) + if begin_section is None or end_section is None: + return False + + lower, upper = sorted((begin_section.rk, end_section.rk)) + return any( + other is not current + and not other.is_deleted() + and other.type == current.type + and other.reach is reach + and other.begin_section is not None + and other.end_section is not None + and max(lower, min(other.begin_rk, other.end_rk)) + < min(upper, max(other.begin_rk, other.end_rk)) + for other in self._data.lst + ) + + def _show_overlap_warning(self): + QMessageBox.warning( + self._table_view, + self._trad["msg_rk_overlap_title"], + self._trad["msg_rk_overlap_text"] + ) + def add(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row) diff --git a/src/View/WeatherParameters/UndoCommand.py b/src/View/WeatherParameters/UndoCommand.py index fe86f9b8..f87ff2ea 100644 --- a/src/View/WeatherParameters/UndoCommand.py +++ b/src/View/WeatherParameters/UndoCommand.py @@ -120,7 +120,7 @@ class SetEndCommand(QUndoCommand): class SetEdgeCommand(QUndoCommand): - def __init__(self, wps, index, edge): + def __init__(self, wps, index, edge, clear_interval=False): QUndoCommand.__init__(self) self._wps = wps diff --git a/src/View/WeatherParameters/translate.py b/src/View/WeatherParameters/translate.py index 7f4fc107..0b8b3162 100644 --- a/src/View/WeatherParameters/translate.py +++ b/src/View/WeatherParameters/translate.py @@ -41,6 +41,14 @@ class WeatherParametersTranslate(MainTranslate): "These values are applied to sections that have not been " "defined using time series." ) + self._dict["msg_rk_overlap_title"] = _translate( + "WeatherParameters", "Overlapping interval" + ) + self._dict["msg_rk_overlap_text"] = _translate( + "WeatherParameters", + "Two weather-parameter intervals of the same type cannot " + "overlap on the same reach." + ) self._dict["rk"] = self._dict["unit_rk"]