diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dd8178d486ebd843149fe4a82ea639f659495178..61a4ab6ffc2839ead886b7c591bfe3dfb34cf098 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -122,7 +122,7 @@ test-pep8: - pip3 install -r ./requirements.txt - pip3 install -U -r ./requirements.txt - pip3 install pycodestyle - - pycodestyle ./src + - pycodestyle --exclude="*_to_*.py" ./src allow_failure: true ######### diff --git a/src/Model/Friction/Friction.py b/src/Model/Friction/Friction.py index db1a60db772c318dd57fdfdf3fe8ab8b5c9ecaac..d3be3cf5980379be21df9ac4201d955279f4919c 100644 --- a/src/Model/Friction/Friction.py +++ b/src/Model/Friction/Friction.py @@ -16,10 +16,14 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from Model.Tools.PamhyrDB import SQLSubModel +logger = logging.getLogger() + class Friction(SQLSubModel): def __init__(self, name: str = "", status=None): @@ -60,7 +64,8 @@ class Friction(SQLSubModel): @classmethod def _db_load(cls, execute, data=None): new = [] - reach = data["parent"] # Reach object + + reach = data["reach"] status = data["status"] stricklers = data["stricklers"].stricklers @@ -69,9 +74,6 @@ class Friction(SQLSubModel): f"FROM friction WHERE reach = {reach.id}" ) - for _ in table: - new.append(None) - for row in table: ind = row[0] # Get stricklers @@ -86,7 +88,9 @@ class Friction(SQLSubModel): sec.begin_strickler = bs sec.end_strickler = es - yield ind, sec + new.append((ind, sec)) + + return new def _db_save(self, execute, data=None): ind = data["ind"] @@ -116,6 +120,10 @@ class Friction(SQLSubModel): def edge(self): return self._edge + @property + def reach(self): + return self._edge + @edge.setter def edge(self, edge): self._edge = edge diff --git a/src/Model/Friction/FrictionList.py b/src/Model/Friction/FrictionList.py index 20c3319764400f516972ad33f4349ebb578b1040..6a5a6c091b63cce40896c52987b9d8f21c1a7c6c 100644 --- a/src/Model/Friction/FrictionList.py +++ b/src/Model/Friction/FrictionList.py @@ -50,10 +50,12 @@ class FrictionList(PamhyrModelList): def _db_load(cls, execute, data=None): new = cls(status=data['status']) - new._lst = Friction._db_load( + ilst = Friction._db_load( execute, data ) + new._lst = list(map(lambda x: x[1], sorted(ilst))) + return new def _db_save(self, execute, data=None): diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 06a6ccf61ecd97e8f888363e5514282c432a3c7f..57c13a373d66dfd29a5a110a91de162e4f9a5b0d 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -116,7 +116,7 @@ class PointXYZ(Point, SQLSubModel): sl = self._sl.id if self._sl is not None else -1 sql = ( - "INSERT OR REPLACE INTO " + + "INSERT INTO " + "geometry_pointXYZ(ind, name, x, y, z, profile, sl) " + "VALUES (" + f"{ind}, '{self._db_format(self._name)}', " + diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index ee662308f33def332297ed0d450d552d74b30669..a04e1a8f15b5b540fa113892e8d08360552435e6 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -214,31 +214,17 @@ class Profile(object): self.points.insert(index, point) self._status.modified() - def delete(self, indexes: int): - """Delete points at index - - Args: - indexes: List of index of points. - - Returns: - Nothing. - """ - points = set( + def delete_i(self, indexes: list): + self._points = list( map( lambda e: e[1], filter( - lambda e: e[0] in indexes, + lambda e: e[0] not in indexes, enumerate(self.points) ) ) ) - self.points = list( - filter( - lambda p: p not in points, - self.points - ) - ) self._status.modified() def delete_points(self, points): @@ -250,7 +236,7 @@ class Profile(object): Returns: Nothing. """ - self.points = list( + self._points = list( filter( lambda p: p not in points, self.points @@ -286,7 +272,7 @@ class Profile(object): elif column == 'z': def predicate(p): return p.z - self.points = sorted( + self._points = sorted( self.points, key=predicate, reverse=is_reversed @@ -298,7 +284,7 @@ class Profile(object): if len(self.points) != len(indexes): logger.critical("Indexes list do not correspond to point list") - self.points = list( + self._points = list( map( lambda x: x[1], sorted( diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index ad3ebb5ea61e9a23b8c8b2c75efe3bdc52f12599..384d930e40537afc0abd4ca3b69949cfd8d8bdfb 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -115,9 +115,6 @@ class ProfileXYZ(Profile, SQLSubModel): f"WHERE reach = {reach.id}" ) - for _ in table: - profiles.append(None) - for row in table: id = row[0] ind = row[1] @@ -132,7 +129,7 @@ class ProfileXYZ(Profile, SQLSubModel): id=id, num=num, name=name, kp=kp, code1=code1, code2=code2, - reach=data["parent"], + reach=reach, status=status ) @@ -151,10 +148,6 @@ class ProfileXYZ(Profile, SQLSubModel): yield ind, new - # profiles[ind] = new - - # return profiles - def _db_save(self, execute, data=None): ok = True ind = data["ind"] @@ -191,11 +184,20 @@ class ProfileXYZ(Profile, SQLSubModel): profile = None try: if len(header) == 0: + name = data[0] + kp = data[1] + reach = data[2] + status = data[3] + profile = cls( - *data + id=-1, + name=name, + kp=kp, + reach=reach, + status=status ) else: - valid_header = {'name', 'reach', 'kp'} + valid_header = {'name', 'reach', 'kp', 'status'} d = {} for i, v in enumerate(data): h = header[i].strip().lower().split(' ')[0] @@ -204,6 +206,7 @@ class ProfileXYZ(Profile, SQLSubModel): profile = cls(**d) except Exception as e: + logger.error(e) raise ClipboardFormatError(header, data) return profile @@ -444,42 +447,57 @@ class ProfileXYZ(Profile, SQLSubModel): return abs(rg.dist(rd)) def get_water_limits(self, z): -#============================================================================== -# détermination des points limites RG et RD pour un niveau d'eau donné -# -# irg et ird sont les premiers indices en partant des rives gauche et -# droite qui correspondent à des points sous la surface de l'eau -# ptX et ptY sont les points interpolés où le plan d'eau intersecte le profil -# known_level est le niveau d'eau pour lequel on a obtenu irg, ird, ptX et ptY -#============================================================================== + # ==================================================================== + # détermination des points limites RG et RD pour un niveau + # d'eau donné + # + # irg et ird sont les premiers indices en partant des rives + # gauche et droite qui correspondent à des points sous la + # surface de l'eau ptX et ptY sont les points interpolés où + # le plan d'eau intersecte le profil known_level est le + # niveau d'eau pour lequel on a obtenu irg, ird, ptX et ptY + # ==================================================================== + # initialisation - irg = -1 ; ird = -1 + irg = -1 + ird = -1 + for i in range(self.number_points): if self.point(i).z <= z: irg = i + for i in reversed(range(self.number_points)): if self.point(i).z <= z: ird = i + # interpolation des points ptX et ptY - if (irg < self.number_points-1): - x=np.interp(z, - [self.point(irg).z,self.point(irg+1).z], - [self.point(irg).x,self.point(irg+1).x]) - y=np.interp(z, - [self.point(irg).z,self.point(irg+1).z], - [self.point(irg).y,self.point(irg+1).y]) - ptX=PointXYZ(x,y,z) + if (irg < self.number_points - 1): + x = np.interp( + z, + [self.point(irg).z, self.point(irg + 1).z], + [self.point(irg).x, self.point(irg + 1).x] + ) + y = np.interp( + z, + [self.point(irg).z, self.point(irg + 1).z], + [self.point(irg).y, self.point(irg + 1).y] + ) + ptX = PointXYZ(x, y, z) else: ptX = self.point(0) if (ird > 0): - x=np.interp(z, - [self.point(ird-1).z,self.point(ird).z], - [self.point(ird-1).x,self.point(ird).x]) - y=np.interp(z, - [self.point(ird).z,self.point(ird-1).z], - [self.point(ird).y,self.point(ird-1).y]) - ptY=PointXYZ(x,y,z) + x = np.interp( + z, + [self.point(ird-1).z, self.point(ird).z], + [self.point(ird-1).x, self.point(ird).x] + ) + y = np.interp( + z, + [self.point(ird).z, self.point(ird - 1).z], + [self.point(ird).y, self.point(ird - 1).y] + ) + ptY = PointXYZ(x, y, z) else: - ptY = self.point(self.number_points-1) + ptY = self.point(self.number_points - 1) - return ptX,ptY + return ptX, ptY diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 5351a9cbbabeb1dd7cf9a62012ccb8542b779168..3c02f16a68541a78b4fd79dfdd09d2c5aeb3f63b 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -61,7 +61,7 @@ class Reach(SQLSubModel): @classmethod def _db_load(cls, execute, data=None): - new = cls(status=data["status"], parent=data["parent"]) + new = cls(status=data["status"], parent=data["reach"]) new._profiles = ProfileXYZ._db_load( execute, diff --git a/src/Model/HydraulicStructures/Basic/HydraulicStructures.py b/src/Model/HydraulicStructures/Basic/HydraulicStructures.py new file mode 100644 index 0000000000000000000000000000000000000000..e7a5af5fa7c383624009fbd421a33675beea86a7 --- /dev/null +++ b/src/Model/HydraulicStructures/Basic/HydraulicStructures.py @@ -0,0 +1,191 @@ +# HydraulicStructures.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from Model.Tools.PamhyrDB import SQLSubModel +from Model.Except import NotImplementedMethodeError + +from Model.HydraulicStructures.Basic.Value import ( + BHSValue +) + +logger = logging.getLogger() + + +class BasicHS(SQLSubModel): + _sub_classes = [ + BHSValue, + ] + _id_cnt = 0 + + def __init__(self, id: int = -1, name: str = "", + status=None): + super(BasicHS, self).__init__() + + self._status = status + + if id == -1: + self.id = BasicHS._id_cnt + else: + self.id = id + + self._name = name + self._type = "" + self._enabled = True + self._data = [] + + BasicHS._id_cnt = max(BasicHS._id_cnt + 1, self.id) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE hydraulic_structures_basic( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + hs INTEGER, + FOREIGN KEY(hs) REFERENCES hydraulic_structures(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 6: + cls._db_create(execute) + + return True + + @classmethod + def _get_ctor_from_type(cls, t): + from Model.HydraulicStructures.Basic.Types import ( + BHS_types, NotDefined, + ) + + res = NotDefined + + if t in BHS_types.keys(): + res = BHS_types[t] + + return res + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + + table = execute( + "SELECT id, name, type, enabled, hs " + + "FROM hydraulic_structures_basic " + + f"WHERE hs = {data['hs_id']} " + ) + + for row in table: + bhs_id = row[0] + name = row[1] + type = row[2] + enabled = (row[3] == 1) + hs_id = row[4] + + ctor = cls._get_ctor_from_type(type) + bhs = ctor( + id=bhs_id, + name=name, + status=data['status'] + ) + + bhs.enabled = enabled + + data['bhs_id'] = bhs_id + bhs._data = BHSValue._db_load( + execute, data + ) + + new.append(bhs) + + return new + + def _db_save(self, execute, data=None): + hs_id = data['hs_id'] + + sql = ( + "INSERT INTO " + + "hydraulic_structures_basic(id, name, type, enabled, hs) " + + "VALUES (" + + f"{self.id}, " + + f"'{self._db_format(self._name)}', " + + f"'{self._db_format(self._type)}', " + + f"{self._db_format(self.enabled)}, " + + f"{hs_id} " + + ")" + ) + execute(sql) + + data['bhs_id'] = self.id + execute( + "DELETE FROM hydraulic_structures_basic_value " + + f"WHERE bhs = {self.id}" + ) + + for values in self._data: + values._db_save(execute, data) + + return True + + def __len__(self): + return len(self._data) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + self._status.modified() + + @property + def type(self): + return self._type + + @type.setter + def type(self, type): + self._type = type + self._status.modified() + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, enabled): + self._enabled = enabled + self._status.modified() + + @property + def parameters(self): + return self._data + + def convert(self, new_type): + return new_type(id=self.id, name=self.name, status=self._status) diff --git a/src/Model/HydraulicStructures/Basic/Types.py b/src/Model/HydraulicStructures/Basic/Types.py new file mode 100644 index 0000000000000000000000000000000000000000..1d1d0930b9159485e5a0976cc5c326e14ba36791 --- /dev/null +++ b/src/Model/HydraulicStructures/Basic/Types.py @@ -0,0 +1,228 @@ +# Types.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from Model.HydraulicStructures.Basic.HydraulicStructures import ( + BasicHS +) + +from Model.HydraulicStructures.Basic.Value import ( + BHSValue +) + + +class NotDefined(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(NotDefined, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "ND" + self._data = [] + + +class SeuilDeversoir(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(SeuilDeversoir, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "S1" + self._data = [ + BHSValue("Largeur", float, 1.0, status=status), + BHSValue("Cote", float, 1.0, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + ] + + +class SeuilTrapezoidal(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(SeuilTrapezoidal, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "S2" + self._data = [ + BHSValue("Largeur", float, 1.0, status=status), + BHSValue("Cote", float, 1.0, status=status), + BHSValue("Cote de mise en charge", float, 9999.999, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Tangeante du demi angle", float, 0.0, status=status), + ] + + +class SeuilTriangulaire(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(SeuilTriangulaire, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "S3" + self._data = [ + BHSValue("Cote", float, 1.0, status=status), + BHSValue("Cote de mise en charge", float, 9999.999, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Tangeante du demi angle", float, 0.0, status=status), + ] + + +class OrificeRectangulaire(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(OrificeRectangulaire, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "OR" + self._data = [ + BHSValue("Largeur", float, 0.0, status=status), + BHSValue("Cote", float, 0.0, status=status), + BHSValue("Cote de mise en charge", float, 9999.999, + status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Cote de mise en charge maximale", float, 9999.999, + status=status), + ] + + +class OrificeCirculaire(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(OrificeCirculaire, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "OC" + self._data = [ + BHSValue("Diametre", float, 0.0, status=status), + BHSValue("Cote", float, 0.0, status=status), + BHSValue("hauteur envasement", float, 0.0, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + ] + + +# class OrificeVoute(BasicHS): +# def __init__(self, id: int = -1, name: str = "", +# status=None): +# super(OrificeVoute, self).__init__( +# id=id, name=name, +# status=status +# ) + +# self._type = "OV" +# self._data = [ +# BHSValue("Cote", float, 1.0, status=status), +# BHSValue("Largeur", float, 1.0, status=status), +# BHSValue("Haut de la voute", float, 0.0, status=status), +# BHSValue("Bas de la voute", float, 0.0, status=status), +# BHSValue("Coefficient de debit", float, 0.4, status=status), +# ] + + +class VanneRectangulaire(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(VanneRectangulaire, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "V1" + self._data = [ + BHSValue("Largeur", float, 1.0, status=status), + BHSValue("Cote", float, 0.0, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Ouverture", float, 1.0, status=status), + BHSValue("Ouverture maximale", float, 1.0, status=status), + ] + + +class VanneRectangulaireSimplifiee(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(VanneRectangulaireSimplifiee, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "V2" + self._data = [ + BHSValue("Largeur", float, 1.0, status=status), + BHSValue("Cote", float, 0.0, status=status), + BHSValue("Coefficient de debit", float, 0.4, status=status), + BHSValue("Ouverture", float, 1.0, status=status), + BHSValue("Ouverture maximale", float, 1.0, status=status), + ] + + +class Borda(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(Borda, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "BO" + self._data = [ + BHSValue("Pas espace", float, 0.1, status=status), + BHSValue("Seuil", float, 0.15, status=status), + BHSValue("Coefficient", float, 0.4, status=status), + ] + + +class UserDefined(BasicHS): + def __init__(self, id: int = -1, name: str = "", + status=None): + super(UserDefined, self).__init__( + id=id, name=name, + status=status + ) + + self._type = "UD" + self._data = [ + BHSValue("Parameter 1", float, 0.0, status=status), + BHSValue("Parameter 2", float, 0.0, status=status), + BHSValue("Parameter 3", float, 0.0, status=status), + BHSValue("Parameter 4", float, 0.0, status=status), + BHSValue("Parameter 5", float, 0.0, status=status), + ] + + +BHS_types = { + "ND": NotDefined, + "S1": SeuilDeversoir, + "S2": SeuilTrapezoidal, + "S3": SeuilTriangulaire, + "OR": OrificeRectangulaire, + "OC": OrificeCirculaire, + # "OV": OrificeVoute, + "V1": VanneRectangulaire, + "V2": VanneRectangulaireSimplifiee, + "BO": Borda, + "UD": UserDefined +} diff --git a/src/Model/HydraulicStructures/Basic/Value.py b/src/Model/HydraulicStructures/Basic/Value.py new file mode 100644 index 0000000000000000000000000000000000000000..ebf4744eda3199dbe89b1e4baa993d528a9e9fab --- /dev/null +++ b/src/Model/HydraulicStructures/Basic/Value.py @@ -0,0 +1,145 @@ +# Value.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from Model.Tools.PamhyrDB import SQLSubModel + + +class BHSValue(SQLSubModel): + _sub_classes = [] + _id_cnt = 0 + + def __init__(self, name: str = "", type=float, value=0.0, + status=None): + super(BHSValue, self).__init__() + + self._status = status + + self._name = name + self._type = type + self._value = type(value) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE hydraulic_structures_basic_value( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + value TEXT NOT NULL, + bhs INTEGER, + FOREIGN KEY(bhs) REFERENCES hydraulic_structures_basic(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 6: + cls._db_create(execute) + + return True + + @classmethod + def _str_to_type(cls, type): + res = str + + if type == "float": + res = float + elif type == "int": + res = int + elif type == "bool": + res = bool + + return res + + @classmethod + def _type_to_str(cls, type): + res = "str" + + if type == float: + res = "float" + elif type == int: + res = "int" + elif type == bool: + res = "bool" + + return res + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + bhs_id = data["bhs_id"] + + table = execute( + "SELECT name, type, value " + + "FROM hydraulic_structures_basic_value " + + f"WHERE bhs = '{bhs_id}'" + ) + + for row in table: + name = row[0] + type = cls._str_to_type(row[1]) + value = row[2] + + val = cls( + name=name, + type=type, + value=value, + status=data['status'] + ) + + new.append(val) + + return new + + def _db_save(self, execute, data=None): + bhs_id = data["bhs_id"] + + sql = ( + "INSERT INTO " + + "hydraulic_structures_basic_value(name, type, value, bhs) " + + "VALUES (" + + f"'{self._db_format(self._name)}', " + + f"'{self._db_format(self._type_to_str(self._type))}', " + + f"'{self._db_format(self._value)}', " + + f"{bhs_id}" + + ")" + ) + execute(sql) + + return True + + @property + def name(self): + return self._name + + @property + def type(self): + return self._type + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + self._value = self._type(value) + self._status.modified() diff --git a/src/Model/HydraulicStructures/HydraulicStructures.py b/src/Model/HydraulicStructures/HydraulicStructures.py new file mode 100644 index 0000000000000000000000000000000000000000..c301439b891216e058011ceb21f402f9bc4ca415 --- /dev/null +++ b/src/Model/HydraulicStructures/HydraulicStructures.py @@ -0,0 +1,285 @@ +# HydraulicStructures.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging +from functools import reduce + +from tools import trace, timer, old_pamhyr_date_to_timestamp + +from Model.Tools.PamhyrDB import SQLSubModel +from Model.Except import NotImplementedMethodeError + +from Model.HydraulicStructures.Basic.HydraulicStructures import BasicHS +from Model.HydraulicStructures.Basic.Types import ( + NotDefined, +) + +logger = logging.getLogger() + + +class HydraulicStructure(SQLSubModel): + _sub_classes = [ + BasicHS, + ] + _id_cnt = 0 + + def __init__(self, id: int = -1, name: str = "", + status=None): + super(HydraulicStructure, self).__init__() + + self._status = status + + if id == -1: + self.id = HydraulicStructure._id_cnt + else: + self.id = id + + self._name = name + self._input_kp = None + self._output_kp = None + self._input_reach = None + self._output_reach = None + self._enabled = True + self._data = [] + + HydraulicStructure._id_cnt = max( + HydraulicStructure._id_cnt + 1, + self.id + ) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE hydraulic_structures( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + input_kp REAL NOT NULL, + output_kp REAL NOT NULL, + input_reach INTEGER, + output_reach INTEGER, + FOREIGN KEY(input_reach) REFERENCES river_reach(id), + FOREIGN KEY(output_reach) REFERENCES river_reach(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 6: + cls._db_create(execute) + + return True + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + + table = execute( + "SELECT id, name, enabled, " + + "input_kp, output_kp, " + + "input_reach, output_reach " + + "FROM hydraulic_structures " + ) + + for row in table: + it = iter(row) + + hs_id = next(it) + name = next(it) + enabled = (next(it) == 1) + input_kp = next(it) + output_kp = next(it) + input_reach_id = next(it) + output_reach_id = next(it) + + hs = cls( + id=hs_id, + name=name, + status=data['status'] + ) + + hs.enabled = enabled + hs.input_kp = input_kp if input_kp != -1 else None + hs.output_kp = output_kp if output_kp != -1 else None + + hs.input_reach, hs.output_reach = reduce( + lambda acc, n: ( + n if n.id == input_reach_id else acc[0], + n if n.id == output_reach_id else acc[1] + ), + data["edges"], + [None, None] + ) + + data['hs_id'] = hs_id + hs._data = BasicHS._db_load(execute, data) + + new.append(hs) + + return new + + def _db_save(self, execute, data=None): + execute(f"DELETE FROM hydraulic_structures WHERE id = {self.id}") + + input_reach_id = -1 + if self._input_reach is not None: + input_reach_id = self._input_reach.id + + output_reach_id = -1 + if self._output_reach is not None: + output_reach_id = self._output_reach.id + + input_kp = -1 + if self.input_kp is not None: + input_kp = self.input_kp + + output_kp = -1 + if self.output_kp is not None: + output_kp = self.output_kp + + sql = ( + "INSERT INTO " + + "hydraulic_structures(" + + " id, name, enabled, input_kp, output_kp, " + + " input_reach, output_reach" + + ") " + + "VALUES (" + + f"{self.id}, '{self._db_format(self._name)}', " + + f"{self._db_format(self.enabled)}, " + + f"{input_kp}, {output_kp}, " + + f"{input_reach_id}, {output_reach_id}" + + ")" + ) + execute(sql) + + data['hs_id'] = self.id + execute( + "DELETE FROM hydraulic_structures_basic " + + f"WHERE hs = {self.id}" + ) + + for basic in self._data: + basic._db_save(execute, data) + + return True + + def __len__(self): + return len(self._data) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + self._status.modified() + + @property + def input_kp(self): + return self._input_kp + + @input_kp.setter + def input_kp(self, input_kp): + self._input_kp = input_kp + self._status.modified() + + @property + def output_kp(self): + return self._output_kp + + @output_kp.setter + def output_kp(self, output_kp): + self._output_kp = output_kp + self._status.modified() + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, enabled): + self._enabled = enabled + self._status.modified() + + @property + def input_reach(self): + return self._input_reach + + @input_reach.setter + def input_reach(self, input_reach): + self._input_reach = input_reach + self._status.modified() + + @property + def output_reach(self): + return self._output_reach + + @output_reach.setter + def output_reach(self, output_reach): + self._output_reach = output_reach + self._status.modified() + + @property + def basic_structures(self): + return self._data.copy() + + def basic_structure(self, index: int): + return self._data[index] + + def add(self, index: int): + value = NotDefined(status=self._status) + self._data.insert(index, value) + self._status.modified() + return value + + def insert(self, index: int, value: BasicHS): + self._data.insert(index, value) + self._status.modified() + + def delete_i(self, indexes): + self._data = list( + map( + lambda e: e[1], + filter( + lambda e: e[0] not in indexes, + enumerate(self._data) + ) + ) + ) + self._status.modified() + + def delete(self, els): + self._data = list( + filter( + lambda e: e not in els, + self._data + ) + ) + self._status.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._status.modified() diff --git a/src/Model/HydraulicStructures/HydraulicStructuresList.py b/src/Model/HydraulicStructures/HydraulicStructuresList.py new file mode 100644 index 0000000000000000000000000000000000000000..549d80745b13099575cc4145d0a1c5a45bb0e773 --- /dev/null +++ b/src/Model/HydraulicStructures/HydraulicStructuresList.py @@ -0,0 +1,78 @@ +# HydraulicStructuresList.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from copy import copy +from tools import trace, timer + +from Model.Tools.PamhyrList import PamhyrModelList +from Model.HydraulicStructures.HydraulicStructures import HydraulicStructure + + +class HydraulicStructureList(PamhyrModelList): + _sub_classes = [ + HydraulicStructure, + ] + + @classmethod + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) + + if data is None: + data = {} + + new._lst = HydraulicStructure._db_load( + execute, data + ) + + return new + + def _db_save(self, execute, data=None): + execute("DELETE FROM hydraulic_structures") + + if data is None: + data = {} + + for hs in self._lst: + hs._db_save(execute, data=data) + + return True + + def new(self, lst, index): + n = HydraulicStructure(status=self._status) + self._lst.insert(index, n) + self._status.modified() + return n + + def __copy__(self): + new = HydraulicStructureList() + + for lst in self._tabs: + new.tabs[lst] = self._tabs[lst].copy() + + return new + + def __deepcopy__(self): + new = HydraulicStructureList() + + for lst in self._tabs: + new.tabs[lst] = self._tabs[lst].deepcopy() + + return new + + def copy(self): + return copy(self) diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py index 439445fa3887ad85087d59f7efd19a88576d2a95..2d88ea688d1571dd06e0437e6249f982099af8d6 100644 --- a/src/Model/InitialConditions/InitialConditions.py +++ b/src/Model/InitialConditions/InitialConditions.py @@ -250,7 +250,7 @@ class InitialConditions(SQLSubModel): ) if new._data is not None: - yield new + return new def _db_save(self, execute, data=None): ok = True @@ -376,7 +376,7 @@ class InitialConditions(SQLSubModel): * (abs(incline) ** (0.5))) ) - elevation= max( + elevation = max( profile.z_min() + height, previous_elevation ) @@ -422,7 +422,7 @@ class InitialConditions(SQLSubModel): ((width * 0.8) * strickler * (abs(incline) ** (0.5))) ) ** (0.6) - elevation= max( + elevation = max( profile.z_min() + height, previous_elevation ) diff --git a/src/Model/Network/Graph.py b/src/Model/Network/Graph.py index d617ba77039b09e21d65925dca13fac464856dfe..c96345b1242d6911ffb2de58b6151e6ccb7bfc09 100644 --- a/src/Model/Network/Graph.py +++ b/src/Model/Network/Graph.py @@ -212,3 +212,16 @@ class Graph(object): def is_enable_edge(self, edge): return edge._enable + + # def get_edge_id(self, reach): + # for i, e in enumerate(self.enable_edges): + # if e.id == reach.id: + # return i + + def get_edge_id(self, reach): + return next( + filter( + lambda e: e[1].id == reach.id, + enumerate(self.enable_edges()) + ) + )[0] diff --git a/src/Model/Reservoir/Reservoir.py b/src/Model/Reservoir/Reservoir.py index 97a6311318e23e56d52c931f5272ad037ad376d2..ebf7e24c883445ac085244dbd22e98c3d7825555 100644 --- a/src/Model/Reservoir/Reservoir.py +++ b/src/Model/Reservoir/Reservoir.py @@ -94,7 +94,11 @@ class Reservoir(SQLSubModel): new_reservoir._node = None if node_id != -1: - new_reservoir._node = next(filter(lambda n: n.id == node_id, data["nodes"])) + new_reservoir._node = next( + filter( + lambda n: n.id == node_id, data["nodes"] + ) + ) new_data = [] table = execute( diff --git a/src/Model/River.py b/src/Model/River.py index 84836625f83f2d7e69031a744d07b3a6aa5803e6..99cf8d2116be0757151a08a9e6b142e0b7a9fde7 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -37,6 +37,9 @@ from Model.Friction.FrictionList import FrictionList from Model.SolverParameters.SolverParametersList import SolverParametersList from Model.SedimentLayer.SedimentLayerList import SedimentLayerList from Model.Reservoir.ReservoirList import ReservoirList +from Model.HydraulicStructures.HydraulicStructuresList import ( + HydraulicStructureList, +) from Solver.Solvers import solver_type_list @@ -154,7 +157,9 @@ class RiverReach(Edge, SQLSubModel): data = {} table = execute( - "SELECT id, name, enable, node1, node2 FROM river_reach") + "SELECT id, name, enable, node1, node2 FROM river_reach" + ) + for row in table: # Update id counter cls._id_cnt = max(cls._id_cnt, row[0]) @@ -169,10 +174,9 @@ class RiverReach(Edge, SQLSubModel): new = cls(id, name, node1, node2, status=data["status"]) new.enable(enable=enable) - data["reach"] = id - data["parent"] = new - new._reach = Reach._db_load(execute, data) + data["reach"] = new + new._reach = Reach._db_load(execute, data) new._frictions = FrictionList._db_load(execute, data) reachs.append(new) @@ -219,6 +223,7 @@ class River(Graph, SQLSubModel): SolverParametersList, SedimentLayerList, ReservoirList, + HydraulicStructureList, ] def __init__(self, status=None): @@ -237,6 +242,9 @@ class River(Graph, SQLSubModel): self._parameters = {} self._sediment_layers = SedimentLayerList(status=self._status) self._reservoir = ReservoirList(status=self._status) + self._hydraulic_structures = HydraulicStructureList( + status=self._status + ) @classmethod def _db_create(cls, execute): @@ -303,6 +311,12 @@ class River(Graph, SQLSubModel): data ) + # Hydraulic Structures + new._hydraulic_structures = HydraulicStructureList._db_load( + execute, + data + ) + # Parameters new._parameters = SolverParametersList._db_load( execute, @@ -319,6 +333,7 @@ class River(Graph, SQLSubModel): objs.append(self._sediment_layers) objs.append(self._stricklers) objs.append(self._reservoir) + objs.append(self._hydraulic_structures) for solver in self._parameters: objs.append(self._parameters[solver]) @@ -363,6 +378,10 @@ class River(Graph, SQLSubModel): def reservoir(self): return self._reservoir + @property + def hydraulic_structures(self): + return self._hydraulic_structures + @property def parameters(self): return self._parameters diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index 070c31ea7e28d40b8bd238ba21f0ffe075fc427d..bf1ca817ca3a3f52d612da579dea82729bba037e 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -142,6 +142,10 @@ class SolverParametersList(PamhyrModelList): insert(v[0], v[1], ind) ind += 1 + if int(release) < 7: + insert("mage_init_internal", "N", ind) + ind += 1 + new = [ ("mage_sediment_masse_volumique", "2650.0"), ("mage_sediment_angle_repos", "40.0"), diff --git a/src/Model/Study.py b/src/Model/Study.py index b163088b8d93312297d38c7ae469f800233269df..033f886747445153e90f65f2241f044905f71825 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -41,7 +41,7 @@ class Study(SQLModel): def __init__(self, filename=None, init_new=True): # Metadata - self._version = "0.0.5" + self._version = "0.0.7" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() @@ -86,7 +86,7 @@ class Study(SQLModel): def is_saved(self): return self.status.is_saved() - def save(self): + def save(self, progress=None): # Save a copy of database fdir, fname = os.path.split(self.filename) @@ -110,7 +110,7 @@ class Study(SQLModel): # Save self.last_save_date = datetime.now() - self._save() + self._save(progress=progress) self.status.save() @property @@ -266,38 +266,54 @@ class Study(SQLModel): return new - def _save(self): + def _save(self, progress=None): + progress = progress if progress is not None else lambda: None + self.execute( f"UPDATE info SET " + f"value='{self._db_format(self.name)}' WHERE key='name'" ) + progress() self.execute( f"UPDATE info SET " + f"value='{self._db_format(self.description)}' " + "WHERE key='description'" ) + progress() self.execute( f"UPDATE info SET " + f"value='{self._time_system}' WHERE key='time_system'" ) + progress() self.execute( f"UPDATE info SET " + f"value='{timestamp(self._date)}' WHERE key='date'" ) + progress() self.execute( f"UPDATE info SET " + f"value='{timestamp(self.creation_date)}' " + "WHERE key='creation_date'" ) + progress() self.execute( f"UPDATE info SET " + f"value='{timestamp(self.last_save_date)}' " + "WHERE key='last_save_date'" ) + progress() - self._save_submodel([self._river]) + self._save_submodel([self._river], data=progress) self.commit() + def sql_save_request_count(self): + return self._count() + + def _count(self): + cnt = self._save_count([self._river]) + logger.debug(cnt) + return cnt + 6 + def close(self): """Close db connection diff --git a/src/Model/Tools/PamhyrDB.py b/src/Model/Tools/PamhyrDB.py index 8ee134fc97f7c725e0cbe1ebdc6fedb94ecb942f..44065709db86ccabe2aaa1e340e3ec673259be46 100644 --- a/src/Model/Tools/PamhyrDB.py +++ b/src/Model/Tools/PamhyrDB.py @@ -21,6 +21,7 @@ import sqlite3 import logging from pathlib import Path +from functools import reduce from tools import SQL from Model.Except import NotImplementedMethodeError @@ -87,11 +88,16 @@ class SQLModel(SQL): raise NotImplementedMethodeError(self, self._update) def _save_submodel(self, objs, data=None): - def fn(sql): return self.execute( - sql, - fetch_one=False, - commit=False - ) + progress = data if data is not None else lambda: None + + def fn(sql): + res = self.execute( + sql, + fetch_one=False, + commit=False + ) + progress() + return res ok = True for obj in objs: @@ -100,9 +106,43 @@ class SQLModel(SQL): self.commit() return ok - def _save(self): + def _save(self, progress=None): raise NotImplementedMethodeError(self, self._save) + def _count(self): + raise NotImplementedMethodeError(self, self._count) + + def _save_count(self, objs, data=None): + counter = { + "insert": 0, + "update": 0, + "delete": 0, + "other": 0, + } + + def fn(sql): + if "insert" in sql.lower(): + counter["insert"] = counter["insert"] + 1 + elif "update" in sql.lower(): + counter["update"] = counter["update"] + 1 + elif "delete" in sql.lower(): + counter["delete"] = counter["delete"] + 1 + else: + counter["other"] = counter["other"] + 1 + return [] + + ok = True + for obj in objs: + ok &= obj._db_save(fn) + + logger.debug(counter) + + return reduce( + lambda acc, k: acc + counter[k], + counter, + 0 + ) + @classmethod def _load(cls, filename=None): raise NotImplementedMethodeError(cls, cls._load) diff --git a/src/Model/Tools/PamhyrDict.py b/src/Model/Tools/PamhyrDict.py index c8cab70a1b0572c4092aa729aee5487e70a612b8..31bc12a6bc9df306df64b719ed977153371830f8 100644 --- a/src/Model/Tools/PamhyrDict.py +++ b/src/Model/Tools/PamhyrDict.py @@ -67,7 +67,7 @@ class PamhyrModelDict(SQLSubModel): if key in self._dict: v = self._dict[key] - if type(v) == types.GeneratorType: + if type(v) is types.GeneratorType: return list(v) return v diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index c4fa286ae9c1bd8ddd938d145b52ad51caef78f1..5ff5f6dcee3e36ecb892b56a7eb89ef6836ff2e2 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -121,6 +121,9 @@ class AbstractSolver(object): def is_stoped(self): return self._status == STATUS.STOPED + def has_results_loaded(self): + self._status = STATUS.NOT_LAUNCHED + @name.setter def name(self, name): self._name = name diff --git a/src/Solver/CommandLine.py b/src/Solver/CommandLine.py index f7f5627c58f2bb38a18a1d5a81ad550d1763e51c..b3614ca3538e92134743772dd6ae8c2d2ed83768 100644 --- a/src/Solver/CommandLine.py +++ b/src/Solver/CommandLine.py @@ -172,12 +172,13 @@ class CommandLineSolver(AbstractSolver): if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" - logger.info(error) + logger.warning(error) return error self._process.start( exe, args, ) + self._process.waitForStarted() return True @@ -191,7 +192,7 @@ class CommandLineSolver(AbstractSolver): if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" - logger.info(error) + logger.warning(error) return error self._process.start( @@ -212,37 +213,48 @@ class CommandLineSolver(AbstractSolver): if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" - logger.info(error) + logger.warning(error) return error self._process.start( exe, args, ) + self._process.waitForStarted() return True def _data_ready(self): # Read process output and put lines in queue s = self._process.readAll().data().decode() + if self._output is not None: for x in s.split('\n'): self._output.put(x) - def _run_next(self, study): - self._step += 1 - if self._step < len(self._runs): - res = self._runs[self._step](study) - if res is not True: - self._output.put(res) - else: - self._status = STATUS.STOPED - def _finished(self, study, exit_code, exit_status): if self._output is not None: self._output.put(exit_code) + logger.debug( + "Process finished with " + + f"code: {exit_code}, status: {exit_status}" + ) + self._run_next(study) + def _run_next(self, study): + self._step += 1 + + if self._step >= len(self._runs): + self._status = STATUS.STOPED + return + + fn = self._runs[self._step] + res = fn(study) + + if res is not True: + self._output.put(res) + def run(self, study, process=None, output_queue=None): self._study = study @@ -255,7 +267,8 @@ class CommandLineSolver(AbstractSolver): # Connect / reconnect signal self._process.readyRead.connect(self._data_ready) self._process.finished.connect( - lambda c, s: self._finished(study, c, s)) + lambda c, s: self._finished(study, c, s) + ) # Prepare running step self._runs = [ @@ -265,6 +278,8 @@ class CommandLineSolver(AbstractSolver): ] self._step = 0 + self._status = STATUS.RUNNING + # Run first step res = self._runs[0](study) if res is not True: diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 22349870d0e3da5b1ae7f644ff4e984d551deae1..00d440984da8bbabe5ce00ebe304a395f9398d34 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -20,7 +20,7 @@ import os import logging import numpy as np -from tools import timer +from tools import timer, trace from Solver.CommandLine import CommandLineSolver from Checker.Mage import MageNetworkGraphChecker @@ -36,7 +36,7 @@ def mage_file_open(filepath, mode): if "w" in mode: # Write header - f.write("* This file is generate by PAMHYR, please don't modify\n") + f.write("* This file is generated by PAMHYR, please don't modify\n") return f @@ -81,6 +81,7 @@ class Mage(CommandLineSolver): ("mage_compute_reach_volume_balance", "y"), ("mage_max_reach_volume_balance", "0.001"), ("mage_min_reach_volume_to_check", "1000.0"), + ("mage_init_internal", " "), ] return lst @@ -374,13 +375,7 @@ class Mage(CommandLineSolver): with mage_file_open(os.path.join(repertory, f"{name}.INI"), "w+") as f: has_ini = False id = 1 - reachs = study.river.edges() - reachs = list( - filter( - lambda e: e.is_enable(), - reachs - ) - ) + reachs = study.river.enable_edges() # TODO put real date... f.write(f"$ date en minutes : 0.00\n") @@ -442,13 +437,149 @@ class Mage(CommandLineSolver): return files + @timer + def _export_SIN(self, study, repertory, qlog, name="0"): + files = [] + + sin_dict = { + "ND": "*", + "S1": "D", "S2": "T", "S3": "T", + "OR": "O", "OC": "B", "OV": "F", + "V1": "V", "V2": "W", + "BO": "A", + "UD": "X", + "PO": "P", + } + + hydraulic_structures = study.river.hydraulic_structures.lst + if len(hydraulic_structures) == 0: + return files + + if qlog is not None: + qlog.put("Export SIN file") + + with mage_file_open(os.path.join(repertory, f"{name}.SIN"), "w+") as f: + files.append(f"{name}.SIN") + + for hs in hydraulic_structures: + if not hs.input_reach.is_enable(): + continue + + f.write( + '* ouvrage au pk ' + + f"{hs.input_kp:>12.1f}" + ' ' + + hs.name + '\n' + ) + + for bhs in hs.basic_structures: + reach_id = study.river.get_edge_id(hs.input_reach) + 1 + param_str = ' '.join( + [ + f'{p:>10.3f}' + for p in self._export_SIN_parameters(bhs) + ] + ) + + f.write( + f"{sin_dict[bhs._type]} " + + f"{reach_id} {hs.input_kp:>12.3f} {param_str} " + + f"{bhs.name}\n" + ) + + return files + + def _export_SIN_parameters(self, bhs): + res = [9999.999] * 5 + + if len(bhs) == 5: + res = self._export_SIN_parameters_5(bhs) + elif len(bhs) == 4: + res = self._export_SIN_parameters_4(bhs) + elif len(bhs) == 3: + res = self._export_SIN_parameters_3(bhs) + + return res + + def _export_SIN_parameters_5(self, bhs): + # S2, OR, V1, V2, UD + return [ + bhs._data[0].value, + bhs._data[1].value, + bhs._data[2].value, + bhs._data[3].value, + bhs._data[4].value, + ] + + def _export_SIN_parameters_4(self, bhs): + # S3, OC + res = [ + bhs._data[0].value, + bhs._data[1].value, + bhs._data[2].value, + bhs._data[3].value, + 0.0, + ] + + if bhs._type == "T": # S3 + res = [0.0] + res[:-1] + + return res + + def _export_SIN_parameters_3(self, bhs): + # S1, BO + if bhs._type == "S1": + res = [ + bhs._data[0].value, + bhs._data[1].value, + 0.0, + bhs._data[2].value, + 9999.99, + ] + else: + res = [ + bhs._data[0].value, + bhs._data[1].value, + bhs._data[2].value, + 0.0, + 0.0, + ] + + return res + + @timer + def _export_DEV(self, study, repertory, qlog, name="0"): + files = [] + + if qlog is not None: + qlog.put("Export DEV file") + + with mage_file_open( + os.path.join( + repertory, f"{name}.DEV" + ), "w+" + ) as f: + reachs = study.river.enable_edges() + + id = 1 + for reach in reachs: + f.write(f"YD{id:3}\n") + f.write(f"YG{id:3}\n") + id += 1 + files.append(f"{name}.DEV") + + return files + @timer def _export_REP(self, study, repertory, files, qlog, name="0"): if qlog is not None: qlog.put("Export REP file") # Write header - with mage_file_open(os.path.join(repertory, f"{name}.REP"), "w+") as f: + with mage_file_open( + os.path.join( + repertory, f"{name}.REP" + ), "w+" + ) as f: f.write("confirmation=non\n") for file in files: @@ -588,8 +719,12 @@ class Mage8(Mage): if name in ["command_line_arguments"]: continue - if name == "mage_compute_reach_volume_balance": - value = "O" if value == "y" else "N" + if name == "compute_reach_volume_balance": + value = "O" if value.lower() == "y" else "N" + + if name == "init_internal": + value = ("p" if value.lower() in ["y", "yes", "true"] + else "") f.write(f"{name} {value}\n") @@ -683,7 +818,9 @@ class Mage8(Mage): self._export_bound_cond(study, repertory, qlog, name=name) files = files + self._export_RUG(study, repertory, qlog, name=name) files = files + self._export_INI(study, repertory, qlog, name=name) + files = files + self._export_SIN(study, repertory, qlog, name=name) files = files + self._export_CAS(study, repertory, qlog, name=name) + files = files + self._export_DEV(study, repertory, qlog, name=name) self._export_REP(study, repertory, files, qlog, name=name) return True @@ -814,12 +951,13 @@ class Mage8(Mage): # Set data for profile RI reach.set(ri, timestamp, key, d) if key == "Z": - profile = study.river.current_reach().reach.profile(ri) - ptX,ptY = profile.get_water_limits(d) + profile = study.river\ + .current_reach()\ + .reach.profile(ri) + ptX, ptY = profile.get_water_limits(d) reach.set(ri, timestamp, "ptX", ptX) reach.set(ri, timestamp, "ptY", ptY) - endline() end = newline().size <= 0 @@ -861,7 +999,7 @@ class Mage8(Mage): logger.debug(f"read_gra: nb_profile = {nb_profile}") logger.debug(f"read_gra: mage_version = {mage_version}") - if mage_version <= 80: + if mage_version < 80: msg = ( "Read GRA files: " + f"Possible incompatible mage version '{mage_version}', " + @@ -929,60 +1067,71 @@ class Mage8(Mage): def ip_to_ri(r, i): return i - reach_offset[r] ts = set() - ind = 0 end = False newline() while not end: n = read_int(1)[0] timestamp = read_float64(1)[0] + with_bedload = read_int(1)[0] - logger.debug(f"read_gra: timestamp = {timestamp} sec") - ts.add(timestamp) + logger.debug(f"read_gra: Number of cross section: {n}") + logger.debug(f"read_gra: Timestamp: {timestamp}") + logger.debug(f"read_gra: Type of bedload: {with_bedload}") endline() - for i in range(n): + npts = [1] * n + if with_bedload == 1: newline() - nsl = read_int(1)[0] + npts = read_int(n) endline() + sum_npts = sum(npts) + logger.debug(f"read_gra: Number of points: {sum_npts}") - # Get current profile id - reach = ip_to_r(i) - ri = ip_to_ri(reach, i) + newline() + nsl = read_int(sum_npts) + logger.debug(f"read_gra: Number of sedimentary layers: {nsl}") + endline() - if nsl > 1: - logger.warning( - "read_gra: " + - "Multiple sediment layers for one profile " + - "is not implemented yet..." - ) + newline() + data = read_float64(sum(nsl) * 3) + endline() - for j in range(nsl): - newline() - nl = read_int(1)[0] - endline() + ts.add(timestamp) - sl = [] + i_pts = 0 + i_data = 0 + # Loop on cross section + for i in range(n): + sec_sl = [] + reach = ip_to_r(i) + p_i = ip_to_ri(reach, i) - for k in range(nl): - newline() - data = read_float64(3) - endline() + # Loop on cross section points + for j in range(npts[i]): + sl = [] - h = data[0] - d50 = data[1] - sigma = data[2] + # Loop on sediment layers + for k in range(nsl[i_pts]): + h = data[i_data] + d50 = data[i_data + 1] + sigma = data[i_data + 2] sl.append((h, d50, sigma)) + i_data += 3 + + i_pts += 1 + sec_sl.append(sl) - reach.set(ri, timestamp, "sl", sl) + reach.set(p_i, timestamp, "sl", sec_sl) - ind += 1 + logger.debug( + f"read_gra: data size = {len(data)} ({i_data} readed)" + ) end = newline().size <= 0 - logger.debug(reachs[0].profiles[0]._data) - results.set("timestamps", ts) + results.set("sediment_timestamps", ts) logger.info(f"read_gra: ... end with {len(ts)} timestamp read") @timer diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index ccec087baf3dd644adbb199619b2a4e94fcadd01..4bdbb5ae72ab145418a4e73fe20d5a6b7d0842b6 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -160,7 +160,7 @@ class EditBoundaryConditionWindow(PamhyrWindow): table_headers=headers, editable_headers=self._data.header, delegates={ - #"time": self._delegate_time, + # "time": self._delegate_time, }, data=self._data, undo=self._undo_stack, diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py index 024dc2889f44a8374ea8c0604d0ccc1b06dc460f..aad78fc35c80eb3ee65c7c519759594152f01e2f 100644 --- a/src/View/BoundaryCondition/Table.py +++ b/src/View/BoundaryCondition/Table.py @@ -115,13 +115,13 @@ class ComboBoxDelegate(QItemDelegate): class TableModel(PamhyrTableModel): - def __init__(self, trad = None, **kwargs): + def __init__(self, trad=None, **kwargs): self._trad = trad self._long_types = {} if self._trad is not None: self._long_types = self._trad.get_dict("long_types") - super(TableModel, self).__init__(trad = trad, **kwargs) + super(TableModel, self).__init__(trad=trad, **kwargs) def _setup_lst(self): self._lst = self._data.boundary_condition diff --git a/src/View/Geometry/Profile/UndoCommand.py b/src/View/Geometry/Profile/UndoCommand.py index c1122a688b7889f563b504a69b0c3e005f9a9ca6..e1bf3ae8593aee207874bd7b8436fa402672af6f 100644 --- a/src/View/Geometry/Profile/UndoCommand.py +++ b/src/View/Geometry/Profile/UndoCommand.py @@ -93,7 +93,7 @@ class AddCommand(QUndoCommand): self._point = None def undo(self): - self._profile.delete([self._index]) + self._profile.delete_i([self._index]) def redo(self): if self._point is None: @@ -119,7 +119,7 @@ class DelCommand(QUndoCommand): self._profile.insert_point(row, point) def redo(self): - self._profile.delete(self._rows) + self._profile.delete_i(self._rows) class SortCommand(QUndoCommand): @@ -181,7 +181,7 @@ class PasteCommand(QUndoCommand): def undo(self): for ind in range(len(self._points)): - self._profile.delete([self._row]) + self._profile.delete_i([self._row]) def redo(self): for point in self._points: diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 9fd3f1aa76a8b55562a20082fd456ec23c29a848..0b241d9497a71578ec3f9736b695500e1a280c98 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -135,16 +135,13 @@ class ProfileWindow(PamhyrWindow): self._tablemodel.blockSignals(False) def index_selected_row(self): - rows = self._tablemodel\ - .selectionModel()\ - .selectedRows() + table = self.find(QTableView, "tableView") + rows = table.selectionModel()\ + .selectedRows() if len(rows) == 0: return 0 - return self._tablemodel\ - .selectionModel()\ - .selectedRows()[0]\ - .row() + return rows[0].row() def add(self): table = self.find(QTableView, "tableView") @@ -213,24 +210,24 @@ class ProfileWindow(PamhyrWindow): self.update_plot() - def copy(self): - rows = self._tablemodel\ - .selectionModel()\ - .selectedRows() - table = [] - table.append(["x", "y", "z", "name"]) + def _copy(self): + table = self.find(QTableView, "tableView") + rows = table.selectionModel().selectedRows() + + data = [] + data.append(["x", "y", "z", "name"]) for row in rows: point = self._profile.point(row.row()) - table.append( + data.append( [ point.x, point.y, point.z, point.name ] ) - self.copyTableIntoClipboard(table) + self.copyTableIntoClipboard(data) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -245,10 +242,10 @@ class ProfileWindow(PamhyrWindow): self._tablemodel.paste(row, header, data) self.update_plot() - def undo(self): + def _undo(self): self._tablemodel.undo() self.update_plot() - def redo(self): + def _redo(self): self._tablemodel.redo() self.update_plot() diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 3bc4ebd15c737fe4cccb1e091980b43340270c87..4f1e0b22de4202b0c5f2b4ff811932a3ace047ad 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -455,7 +455,7 @@ class GeometryWindow(PamhyrWindow): .selectedRows() table = [] - table.append(["name", "kp"]) + # table.append(["name", "kp"]) for row in rows: profile = self._reach.profile(row.row()) @@ -471,13 +471,17 @@ class GeometryWindow(PamhyrWindow): if len(data) == 0: return - if len(header) != 0: - header.append("reach") + # if len(header) != 0: + # header.append("reach") + # header.append("status") + for row in data: row.append(self._reach) + row.append(self._study.river._status) row = self.index_selected_row() - self._tablemodel.paste(row, header, data) + # self._tablemodel.paste(row, header, data) + self._tablemodel.paste(row, [], data) self.select_current_profile() def _undo(self): diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..ed81c13166ed87864d90f72026efbea6f0833789 --- /dev/null +++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Table.py @@ -0,0 +1,275 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QMessageBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.HydraulicStructures.BasicHydraulicStructures.UndoCommand import ( + SetNameCommand, SetTypeCommand, + SetEnabledCommand, AddCommand, DelCommand, + SetValueCommand, +) +from Model.HydraulicStructures.Basic.Types import BHS_types + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, trad=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._trad = trad + + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + lst = list( + map( + lambda k: self._long_types[k], + BHS_types.keys() + ) + ) + self.editor.addItems(lst) + + 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, trad=None, **kwargs): + self._trad = trad + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + super(TableModel, self).__init__(trad=trad, **kwargs) + + def rowCount(self, parent): + return len(self._lst) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._data.basic_structure(row).name + elif self._headers[column] == "type": + return self._long_types[self._data.basic_structure(row).type] + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._data, row, value + ) + ) + elif self._headers[column] == "type": + if self._question_set_type(): + key = next( + k for k, v in self._long_types.items() + if v == value + ) + + self._undo.push( + SetTypeCommand( + self._data, row, BHS_types[key] + ) + ) + except Exception as e: + logger.error(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def _question_set_type(self): + question = QMessageBox(self._parent) + + question.setWindowTitle(self._trad['msg_type_change_title']) + question.setText(self._trad['msg_type_change_text']) + question.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) + question.setIcon(QMessageBox.Question) + + res = question.exec() + return res == QMessageBox.Ok + + 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]) + + self._undo.push( + DelCommand( + self._data, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def enabled(self, row, enabled, parent=QModelIndex()): + self._undo.push( + SetEnabledCommand( + self._lst, row, enabled + ) + ) + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() + + +class ParametersTableModel(PamhyrTableModel): + def __init__(self, trad=None, **kwargs): + self._trad = trad + self._long_types = {} + + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + + self._hs_index = None + + super(ParametersTableModel, self).__init__(trad=trad, **kwargs) + + def rowCount(self, parent): + if self._hs_index is None: + return 0 + + return len( + self._data.basic_structure(self._hs_index) + ) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + if self._hs_index is None: + return QVariant() + + row = index.row() + column = index.column() + + hs = self._data.basic_structure(self._hs_index) + + if self._headers[column] == "name": + return hs.parameters[row].name + elif self._headers[column] == "value": + return str(hs.parameters[row].value) + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + if self._hs_index is None: + return QVariant() + + row = index.row() + column = index.column() + + try: + if self._headers[column] == "value": + self._undo.push( + SetValueCommand( + self._data.basic_structure(self._hs_index), + row, value + ) + ) + except Exception as e: + logger.error(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def update_hs_index(self, index): + self._hs_index = index + self.layoutChanged.emit() diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py new file mode 100644 index 0000000000000000000000000000000000000000..6681219e8dab5d517e579f805d31b99d5480015e --- /dev/null +++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Translate.py @@ -0,0 +1,85 @@ +# translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class BasicHydraulicStructuresTranslate(PamhyrTranslate): + def __init__(self): + super(BasicHydraulicStructuresTranslate, self).__init__() + + self._dict['msg_type_change_title'] = _translate( + "BasicHydraulicStructures", + "Change hydraulic structure type" + ) + + self._dict['msg_type_change_text'] = _translate( + "BasicHydraulicStructures", + "Do you want to change the hydraulic structure type and reset \ +hydraulic structure values?" + ) + + self._sub_dict["long_types"] = { + "ND": _translate( + "BasicHydraulicStructures", "Not defined" + ), + "S1": _translate( + "BasicHydraulicStructures", "Seuil déversoir" + ), + "S2": _translate( + "BasicHydraulicStructures", "Seuil trapezoidal" + ), + "S3": _translate( + "BasicHydraulicStructures", "Seuil triangulaire" + ), + "OR": _translate( + "BasicHydraulicStructures", "Orifice rectangulaire" + ), + "OC": _translate( + "BasicHydraulicStructures", "Orifice circulaire" + ), + "OV": _translate( + "BasicHydraulicStructures", "Orifice voute" + ), + "V1": _translate( + "BasicHydraulicStructures", "Vanne rectangulaire" + ), + "V2": _translate( + "BasicHydraulicStructures", "Vanne rectangulaire simplifiée" + ), + "BO": _translate( + "BasicHydraulicStructures", "Perte de charge à la Borda" + ), + "UD": _translate( + "BasicHydraulicStructures", "User defined" + ), + } + + self._sub_dict["table_headers"] = { + "name": _translate("BasicHydraulicStructures", "Name"), + "type": _translate("BasicHydraulicStructures", "Type"), + } + + self._sub_dict["table_headers_parameters"] = { + "name": _translate("BasicHydraulicStructures", "Name"), + "value": _translate("BasicHydraulicStructures", "Value"), + } diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py b/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..124a446ca9c548498a43acfb0efb57b5d0ceaf3d --- /dev/null +++ b/src/View/HydraulicStructures/BasicHydraulicStructures/UndoCommand.py @@ -0,0 +1,153 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + + +class SetNameCommand(QUndoCommand): + def __init__(self, hs, index, new_value): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._old = self._hs.basic_structure(self._index).name + self._new = str(new_value) + + def undo(self): + self._hs.basic_structure(self._index).name = self._old + + def redo(self): + self._hs.basic_structure(self._index).name = self._new + + +class SetTypeCommand(QUndoCommand): + def __init__(self, hs, index, new_type): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._type = new_type + self._old = self._hs.basic_structure(self._index) + self._new = self._hs.basic_structure(self._index)\ + .convert(self._type) + + def undo(self): + self._hs.delete_i([self._index]) + self._hs.insert(self._index, self._old) + + def redo(self): + self._hs.delete_i([self._index]) + self._hs.insert(self._index, self._new) + + +class SetEnabledCommand(QUndoCommand): + def __init__(self, hs, index, enabled): + QUndoCommand.__init__(self) + + self._hs = hs + self._index = index + self._old = not enabled + self._new = enabled + + def undo(self): + self._hs.basic_structure(self._index).enabled = self._old + + def redo(self): + self._hs.basic_structure(self._index).enabled = self._new + + +class AddCommand(QUndoCommand): + def __init__(self, hs, index): + QUndoCommand.__init__(self) + + self._hs = hs + + self._index = index + self._new = None + + def undo(self): + self._hs.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._hs.add(self._index) + else: + self._hs.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, hs, rows): + QUndoCommand.__init__(self) + + self._hs = hs + + self._rows = rows + + self._bhs = [] + for row in rows: + self._bhs.append((row, self._hs.basic_structure(row))) + + def undo(self): + for row, el in self._bhs: + self._hs.insert(row, el) + + def redo(self): + self._hs.delete_i(self._rows) + + +class PasteCommand(QUndoCommand): + def __init__(self, hs, row, h_s): + QUndoCommand.__init__(self) + + self._hs = hs + + self._row = row + self._bhs = deepcopy(h_s) + self._bhs.reverse() + + def undo(self): + self._hs.delete_i(range(self._row, self._row + len(self._bhs))) + + def redo(self): + for r in self._bhs: + self._hs.insert(self._row, r) + +#################################### +# Basic hydraulic structure values # +#################################### + + +class SetValueCommand(QUndoCommand): + def __init__(self, bhs, index, value): + QUndoCommand.__init__(self) + + self._bhs = bhs + self._index = index + self._old = self._bhs.parameters[self._index].value + self._new = self._bhs.parameters[self._index].type(value) + + def undo(self): + self._bhs.parameters[self._index].value = self._old + + def redo(self): + self._bhs.parameters[self._index].value = self._new diff --git a/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py b/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..ca4b26800ea639e752c28419725829ae5e82781c --- /dev/null +++ b/src/View/HydraulicStructures/BasicHydraulicStructures/Window.py @@ -0,0 +1,265 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from tools import timer, trace + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, QItemSelectionModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.HydraulicStructures.PlotAC import PlotAC + +from View.HydraulicStructures.BasicHydraulicStructures.Table import ( + ComboBoxDelegate, TableModel, ParametersTableModel, +) + +from View.Network.GraphWidget import GraphWidget +from View.HydraulicStructures.BasicHydraulicStructures.Translate import ( + BasicHydraulicStructuresTranslate +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class BasicHydraulicStructuresWindow(PamhyrWindow): + _pamhyr_ui = "BasicHydraulicStructures" + _pamhyr_name = "Basic Hydraulic Structures" + + def __init__(self, data=None, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name + + super(BasicHydraulicStructuresWindow, self).__init__( + title=name, + study=study, + config=config, + trad=BasicHydraulicStructuresTranslate(), + parent=parent + ) + + self._hash_data.append(data) + + self._hs = data + + self.setup_table() + self.setup_checkbox() + self.setup_plot() + self.setup_connections() + + self.update() + + def setup_table(self): + self.setup_table_bhs() + self.setup_table_bhs_parameters() + + def setup_table_bhs(self): + self._table = None + + self._delegate_type = ComboBoxDelegate( + trad=self._trad, + parent=self + ) + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "type"], + delegates={ + "type": self._delegate_type, + }, + trad=self._trad, + data=self._hs, + undo=self._undo_stack, + parent=self, + ) + + selectionModel = table.selectionModel() + index = table.model().index(0, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def setup_table_bhs_parameters(self): + self._table_parameters = None + + table = self.find(QTableView, f"tableView_2") + self._table_parameters = ParametersTableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers_parameters"), + editable_headers=["value"], + delegates={}, + trad=self._trad, + data=self._hs, + undo=self._undo_stack, + parent=self, + ) + + def setup_checkbox(self): + self._checkbox = self.find(QCheckBox, f"checkBox") + self._set_checkbox_state() + + def setup_plot(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) + self.plot_layout.addWidget(self.canvas) + + reach = self._hs.input_reach + profile_kp = self._hs.input_kp + if profile_kp is not None: + profiles = reach.reach.get_profiles_from_kp(float(profile_kp)) + else: + profiles = None + if profiles is not None: + profile = profiles[0] + else: + profile = None + + self.plot_ac = PlotAC( + canvas=self.canvas, + river=self._study.river, + reach=self._hs.input_reach, + profile=profile, + toolbar=self.toolbar + ) + self.plot_ac.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self._checkbox.clicked.connect(self._set_basic_structure_state) + + table = self.find(QTableView, "tableView") + table.selectionModel()\ + .selectionChanged\ + .connect(self.update) + + self._table.dataChanged.connect(self.update) + self._table.layoutChanged.connect(self.update) + + def index_selected(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0] + else: + return None + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0].row() + else: + return None + + 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._hs) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + 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 _set_checkbox_state(self): + row = self.index_selected_row() + + if row is None: + self._checkbox.setEnabled(False) + self._checkbox.setChecked(True) + else: + self._checkbox.setEnabled(True) + self._checkbox.setChecked(self._hs.basic_structure(row).enabled) + + def _set_basic_structure_state(self): + row = self.index_selected_row() + + if row is not None: + self._table.enabled( + row, + self._checkbox.isChecked() + ) + + def update(self): + self._set_checkbox_state() + self._update_parameters_table() + + def _update_parameters_table(self): + row = self.index_selected_row() + self._table_parameters.update_hs_index(row) diff --git a/src/View/HydraulicStructures/PlotAC.py b/src/View/HydraulicStructures/PlotAC.py new file mode 100644 index 0000000000000000000000000000000000000000..4d171968945997795e09919d1ee70e8cf338146a --- /dev/null +++ b/src/View/HydraulicStructures/PlotAC.py @@ -0,0 +1,133 @@ +# PlotAC.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from tools import timer +from View.Tools.PamhyrPlot import PamhyrPlot +from matplotlib import pyplot as plt + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + + +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + river=None, reach=None, profile=None, + parent=None): + super(PlotAC, self).__init__( + canvas=canvas, + trad=trad, + data=river, + toolbar=toolbar, + parent=parent + ) + + self._current_reach = reach + self._current_profile = profile + + @property + def river(self): + return self.data + + @river.setter + def river(self, river): + self.data = river + + @timer + def draw(self, highlight=None): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + self.line_kp = None + return + + if self._current_reach is None: + self.line_kp = None + return + + reach = self._current_reach + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='black', fontsize=11 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Elevation (m)"), + color='black', fontsize=11 + ) + + if self._current_profile is None: + self.line_kp = None + else: + profile = self._current_profile + x = profile.get_station() + z = profile.z() + + self.line_kp, = self.canvas.axes.plot( + x, z, + linestyle="solid", + lw=1.8, + color='grey', + ) + + self.canvas.axes.relim() + self.canvas.axes.autoscale_view() + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + def set_reach(self, reach): + self._current_reach = reach + self.update() + + def set_profile(self, profile): + self._current_profile = profile + self.update() + + def update(self): + if self.line_kp is None: + self.draw() + return + + if self._current_reach is None or self._current_profile is None: + self.clear() + return + + profile = self._current_profile + x = profile.get_station() + z = profile.z() + + self.line_kp.set_data(x, z) + + self.canvas.axes.relim() + self.canvas.axes.autoscale_view() + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + + def clear(self): + if self.line_kp is not None: + self.line_kp.set_data([], []) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/HydraulicStructures/PlotKPC.py b/src/View/HydraulicStructures/PlotKPC.py new file mode 100644 index 0000000000000000000000000000000000000000..f72cf7fc1d41cdbe15d22ac761bbb6d232a07c0c --- /dev/null +++ b/src/View/HydraulicStructures/PlotKPC.py @@ -0,0 +1,161 @@ +# PlotKPC.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from tools import timer +from View.Tools.PamhyrPlot import PamhyrPlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +from matplotlib.collections import LineCollection + +_translate = QCoreApplication.translate + + +class PlotKPC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + river=None, reach=None, profile=None, + parent=None): + super(PlotKPC, self).__init__( + canvas=canvas, + trad=trad, + data=river, + toolbar=toolbar, + parent=parent + ) + + self._current_reach = reach + self._current_profile = profile + + @property + def river(self): + return self.data + + @river.setter + def river(self, river): + self.data = river + + @timer + def draw(self, highlight=None): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + self.profile = None + self.line_kp_zmin_zmax = None + self.line_kp_zmin = None + return + + if self._current_reach is None: + self.profile = None + self.line_kp_zmin_zmax = None + self.line_kp_zmin = None + return + + reach = self._current_reach + + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Elevation (m)"), + color='black', fontsize=11 + ) + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "KP (m)"), + color='black', fontsize=11 + ) + + kp = reach.reach.get_kp() + z_min = reach.reach.get_z_min() + z_max = reach.reach.get_z_max() + + self.line_kp_zmin, = self.canvas.axes.plot( + kp, z_min, + color='grey', lw=1. + ) + + if len(kp) != 0: + self.line_kp_zmin_zmax = self.canvas.axes.vlines( + x=kp, + ymin=z_min, ymax=z_max, + color='b', + lw=1. + ) + + if self._current_profile is None: + self.profile = None + else: + self.profile, = self.canvas.axes.plot( + [self._current_profile.kp, self._current_profile.kp], + [self._current_profile.z_min(), self._current_profile.z_max()], + color='red', lw=1. + ) + + self.canvas.axes.relim() + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + def set_reach(self, reach): + self._current_reach = reach + self._current_profile = None + self.update() + + def set_profile(self, profile): + self._current_profile = profile + self.update_profil() + + def update(self): + self.draw() + + def update_profil(self): + reach = self._current_reach + kp = reach.reach.get_kp() + z_min = reach.reach.get_z_min() + z_max = reach.reach.get_z_max() + + if self.profile is None: + self.draw() + else: + self.profile.set_data( + [self._current_profile.kp, self._current_profile.kp], + [self._current_profile.z_min(), self._current_profile.z_max()], + ) + + self.canvas.axes.relim() + self.canvas.axes.autoscale_view() + self.canvas.figure.canvas.draw_idle() + + def clear(self): + if self.profile is not None: + self.profile.set_data([], []) + + if self.line_kp_zmin_zmax is not None: + self.line_kp_zmin_zmax.remove() + self.line_kp_zmin_zmax = None + + if self.line_kp_zmin is not None: + self.line_kp_zmin.set_data([], []) + + self.canvas.figure.canvas.draw_idle() + + def clear_profile(self): + if self.profile is not None: + self.profile.set_data([], []) + + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/HydraulicStructures/Table.py b/src/View/HydraulicStructures/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..210cf1e8ec7d6d0d9c4de8bc8170e8d637167cf5 --- /dev/null +++ b/src/View/HydraulicStructures/Table.py @@ -0,0 +1,215 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.HydraulicStructures.UndoCommand import ( + SetNameCommand, SetReachCommand, SetKpCommand, + SetEnabledCommand, AddCommand, DelCommand, +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, trad=None, parent=None, mode="reaches"): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._trad = trad + self._mode = mode + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + val = [] + if self._mode == "kp": + reach = self._data.hydraulic_structures\ + .get(index.row())\ + .input_reach + if reach is not None: + val = list( + map( + lambda kp: str(kp), reach.reach.get_kp() + ) + ) + else: + val = list( + map( + lambda n: n.name, self._data.edges() + ) + ) + + self.editor.addItems( + [_translate("Hydraulic structure", "Not associated")] + + val + ) + + self.editor.setCurrentText(str(index.data(Qt.DisplayRole))) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data._hydraulic_structures + + def rowCount(self, parent): + return len(self._lst) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._lst.get(row).name + elif self._headers[column] == "reach": + n = self._lst.get(row).input_reach + if n is None: + return _translate("Hydraulic structure", "Not associated") + return n.name + elif self._headers[column] == "kp": + n = self._lst.get(row).input_kp + if n is None: + return _translate("Hydraulic structure", "Not associated") + return n + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + na = _translate("Hydraulic structure", "Not associated") + + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._lst, row, value + ) + ) + elif self._headers[column] == "reach": + if value == na: + value = None + + self._undo.push( + SetReachCommand( + self._lst, row, self._data.edge(value) + ) + ) + elif self._headers[column] == "kp": + if value == na: + value = None + + self._undo.push( + SetKpCommand( + self._lst, row, 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._lst, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._lst, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def enabled(self, row, enabled, parent=QModelIndex()): + self._undo.push( + SetEnabledCommand( + self._lst, row, enabled + ) + ) + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/HydraulicStructures/Translate.py b/src/View/HydraulicStructures/Translate.py new file mode 100644 index 0000000000000000000000000000000000000000..1db3faeca39e54c5d391af1f631598809e3c3d52 --- /dev/null +++ b/src/View/HydraulicStructures/Translate.py @@ -0,0 +1,34 @@ +# translate.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class HydraulicStructuresTranslate(PamhyrTranslate): + def __init__(self): + super(HydraulicStructuresTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("HydraulicStructures", "Name"), + "reach": _translate("HydraulicStructures", "Reach"), + "kp": _translate("HydraulicStructures", "Kp"), + } diff --git a/src/View/HydraulicStructures/UndoCommand.py b/src/View/HydraulicStructures/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..9c47ccf12256fb436862ef1316db9814ddfd52a2 --- /dev/null +++ b/src/View/HydraulicStructures/UndoCommand.py @@ -0,0 +1,159 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +logger = logging.getLogger() + + +class SetNameCommand(QUndoCommand): + def __init__(self, h_s_lst, index, new_value): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).name + self._new = str(new_value) + + def undo(self): + self._h_s_lst.get(self._index).name = self._old + + def redo(self): + self._h_s_lst.get(self._index).name = self._new + + +class SetReachCommand(QUndoCommand): + def __init__(self, h_s_lst, index, reach): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).input_reach + self._new = reach + self._old_kp = self._h_s_lst.get(self._index).input_kp + self._new_kp = None + + def undo(self): + i = self._h_s_lst.get(self._index) + i.input_reach = self._old + i.input_kp = self._old_kp + + def redo(self): + i = self._h_s_lst.get(self._index) + i.input_reach = self._new + i.input_kp = self._new_kp + + +class SetKpCommand(QUndoCommand): + def __init__(self, h_s_lst, index, kp): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = self._h_s_lst.get(self._index).input_kp + self._new = kp + + def undo(self): + self._h_s_lst.get(self._index).input_kp = self._old + + def redo(self): + self._h_s_lst.get(self._index).input_kp = self._new + + +class SetEnabledCommand(QUndoCommand): + def __init__(self, h_s_lst, index, enabled): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + self._index = index + self._old = not enabled + self._new = enabled + + def undo(self): + self._h_s_lst.get(self._index).enabled = self._old + + def redo(self): + self._h_s_lst.get(self._index).enabled = self._new + + +class AddCommand(QUndoCommand): + def __init__(self, h_s_lst, index): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._index = index + self._new = None + + def undo(self): + self._h_s_lst.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._h_s_lst.new(self._h_s_lst, self._index) + else: + self._h_s_lst.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, h_s_lst, rows): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._rows = rows + + self._h_s = [] + for row in rows: + self._h_s.append((row, self._h_s_lst.get(row))) + self._h_s.sort() + + def undo(self): + for row, el in self._h_s: + self._h_s_lst.insert(row, el) + + def redo(self): + self._h_s_lst.delete_i(self._rows) + + +class PasteCommand(QUndoCommand): + def __init__(self, h_s_lst, row, h_s): + QUndoCommand.__init__(self) + + self._h_s_lst = h_s_lst + + self._row = row + self._h_s = deepcopy(h_s) + self._h_s.reverse() + + def undo(self): + self._h_s_lst.delete_i( + self._tab, + range(self._row, self._row + len(self._h_s)) + ) + + def redo(self): + for r in self._h_s: + self._h_s_lst.insert(self._row, r) diff --git a/src/View/HydraulicStructures/Window.py b/src/View/HydraulicStructures/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..b0b80bf9ec2c6b55cb87395fe0bff80d5ffd9cfd --- /dev/null +++ b/src/View/HydraulicStructures/Window.py @@ -0,0 +1,313 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from tools import timer, trace + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, QItemSelectionModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.HydraulicStructures.PlotAC import PlotAC +from View.HydraulicStructures.PlotKPC import PlotKPC + +from View.HydraulicStructures.Table import ( + TableModel, ComboBoxDelegate +) + +from View.Network.GraphWidget import GraphWidget +from View.HydraulicStructures.Translate import HydraulicStructuresTranslate + +from View.HydraulicStructures.BasicHydraulicStructures.Window import ( + BasicHydraulicStructuresWindow +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class HydraulicStructuresWindow(PamhyrWindow): + _pamhyr_ui = "HydraulicStructures" + _pamhyr_name = "Hydraulic Structures" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name + + super(HydraulicStructuresWindow, self).__init__( + title=name, + study=study, + config=config, + trad=HydraulicStructuresTranslate(), + parent=parent + ) + + self._hs_lst = self._study.river._hydraulic_structures + + self.setup_table() + self.setup_checkbox() + self.setup_plots() + self.setup_connections() + + self.update() + + def setup_table(self): + self._table = None + + self._delegate_reach = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + parent=self, + mode="reaches" + ) + self._delegate_kp = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + parent=self, + mode="kp" + ) + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "reach", "kp"], + delegates={ + "reach": self._delegate_reach, + "kp": self._delegate_kp, + }, + trad=self._trad, + data=self._study.river, + undo=self._undo_stack, + ) + + selectionModel = table.selectionModel() + index = table.model().index(0, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def setup_checkbox(self): + self._checkbox = self.find(QCheckBox, f"checkBox") + self._set_checkbox_state() + + def setup_plots(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) + self.plot_layout.addWidget(self.canvas) + + self.plot_kpc = PlotKPC( + canvas=self.canvas, + river=self._study.river, + reach=None, + profile=None, + toolbar=self.toolbar + ) + self.plot_kpc.draw() + + self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_2.setObjectName("canvas_2") + self.toolbar_2 = PamhyrPlotToolbar( + self.canvas_2, self + ) + self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout_2.addWidget(self.toolbar_2) + self.plot_layout_2.addWidget(self.canvas_2) + + self.plot_ac = PlotAC( + canvas=self.canvas_2, + river=self._study.river, + reach=None, + profile=None, + toolbar=self.toolbar_2 + ) + self.plot_ac.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self.find(QAction, "action_edit").triggered.connect(self.edit) + self._checkbox.clicked.connect(self._set_structure_state) + + table = self.find(QTableView, "tableView") + table.selectionModel()\ + .selectionChanged\ + .connect(self.update) + + self._table.dataChanged.connect(self.update) + self._table.layoutChanged.connect(self.update) + + def index_selected(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0] + else: + return None + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + r = table.selectionModel().selectedRows() + + if len(r) > 0: + return r[0].row() + else: + return None + + 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._hs_lst) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + 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._hs_lst.get(row) + print(row) + print(data) + if self.sub_window_exists( + BasicHydraulicStructuresWindow, + data=[self._study, None, data] + ): + continue + + win = BasicHydraulicStructuresWindow( + data=data, + study=self._study, + parent=self + ) + win.show() + + def _set_checkbox_state(self): + row = self.index_selected_row() + if row is None: + self._checkbox.setEnabled(False) + self._checkbox.setChecked(True) + else: + self._checkbox.setEnabled(True) + self._checkbox.setChecked(self._hs_lst.get(row).enabled) + + def _set_structure_state(self): + row = self.index_selected_row() + if row is not None: + self._table.enabled( + row, + self._checkbox.isChecked() + ) + + def update(self): + self._set_checkbox_state() + self._update_clear_plot() + + def _update_clear_plot(self): + rows = self.index_selected_rows() + + if len(rows) == 0 or len(self._hs_lst) == 0: + self._update_clear_all() + return + + reach = self._hs_lst.get(rows[0]).input_reach + if reach is not None: + self.plot_kpc.set_reach(reach) + self.plot_ac.set_reach(reach) + + profile_kp = self._hs_lst.get(rows[0]).input_kp + if profile_kp is not None: + profiles = reach.reach\ + .get_profiles_from_kp( + float(profile_kp) + ) + + if profiles is not None: + profile = profiles[0] + + self.plot_kpc.set_profile(profile) + self.plot_ac.set_profile(profile) + else: + self._update_clear_profile() + else: + self._update_clear_profile() + else: + self._update_clear_all() + + def _update_clear_all(self): + self.plot_kpc.clear() + self.plot_ac.clear() + + def _update_clear_profile(self): + self.plot_ac.clear() + self.plot_kpc.clear_profile() diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 745fd15e143e13320ab67ed85df45dfb7dfe1436..897703a469c147761234d2ff7d2003775d95d342 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -101,7 +101,7 @@ class EditLateralContributionWindow(PamhyrWindow): table_headers=headers, editable_headers=self._data.header, delegates={ - #"time": self._delegate_time, + # "time": self._delegate_time, }, data=self._data, undo=self._undo_stack, diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index 8a9a2e8e740c48a37e08d9662c9a49defd77142b..a6f04af5a494680de1dba05b0a73761459f45691 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -107,7 +107,7 @@ class LateralContributionWindow(PamhyrWindow): self._table[t] = TableModel( table_view=table, table_headers=self._trad.get_dict("table_headers"), - editable_headers=True, + editable_headers=self._trad.get_dict("table_headers"), delegates={ "type": self._delegate_type, "edge": self._delegate_edge, diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 04ed5f94267aad9aad4e2419b6aea1fab59b47f1..4ce865db72b513fb371b577034cf67677894c8c2 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -28,12 +28,12 @@ from PyQt5.QtGui import ( ) from PyQt5.QtCore import ( - QTranslator, QEvent, QUrl, + Qt, QTranslator, QEvent, QUrl, ) from PyQt5.QtWidgets import ( QMainWindow, QApplication, QAction, QFileDialog, QShortcut, QMenu, QToolBar, - QMessageBox, + QMessageBox, QProgressDialog, ) from PyQt5.uic import loadUi @@ -48,6 +48,7 @@ from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.Reservoir.Window import ReservoirWindow +from View.HydraulicStructures.Window import HydraulicStructuresWindow from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow @@ -102,7 +103,8 @@ define_model_action = [ "action_menu_boundary_conditions", "action_menu_initial_conditions", "action_menu_edit_friction", "action_menu_edit_lateral_contribution", "action_menu_run_solver", "action_menu_sediment_layers", - "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs" + "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs", + "action_menu_edit_hydraulic_structures" ] action = ( @@ -193,6 +195,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_geometry": self.open_geometry, "action_menu_boundary_conditions": self.open_boundary_cond, "action_menu_edit_reservoirs": self.open_reservoir, + "action_menu_edit_hydraulic_structures": + self.open_hydraulic_structures, "action_menu_initial_conditions": self.open_initial_conditions, "action_menu_edit_friction": self.open_frictions, "action_menu_edit_lateral_contribution": self.open_lateral_contrib, @@ -279,8 +283,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Nothing """ self.update_enable_action() - # Maximise window - #self.showMaximized() + # self.showMaximized() def set_debug_lvl(self, debug=True): if debug: @@ -403,8 +406,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self._study.is_saved: return + sql_request_count = self._study.sql_save_request_count() + progress = QProgressDialog( + "Saving...", None, + 0, sql_request_count, + parent=self + ) + progress.setWindowModality(Qt.WindowModal) + progress.setValue(0) + logger.info("Save...") - self._study.save() + self._study.save( + progress=lambda: progress.setValue(progress.value() + 1) + ) + logger.info("Done") def save_as_study(self): """Save current study as new file @@ -425,7 +440,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): else: self._study.filename = file_name + ".pamhyr" - self._study.save() + sql_request_count = self._study.sql_save_request_count() + progress = QProgressDialog( + "Saving...", None, + 0, sql_request_count, + parent=self + ) + progress.setWindowModality(Qt.WindowModal) + progress.setValue(0) + + logger.info("Save...") + self._study.save( + progress=lambda: progress.setValue(progress.value() + 1) + ) + logger.info("Done") ################## # MSG AND DIALOG # @@ -628,6 +656,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): reservoir = ReservoirWindow(study=self._study, parent=self) reservoir.show() + def open_hydraulic_structures(self): + if self.sub_window_exists( + HydraulicStructuresWindow, + data=[self._study, None] + ): + return + + hydraulic_structures = HydraulicStructuresWindow( + study=self._study, + parent=self + ) + hydraulic_structures.show() + def open_lateral_contrib(self): if self.sub_window_exists( LateralContributionWindow, @@ -635,7 +676,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ): return - lateral = LateralContributionWindow(study=self._study, parent=self) + lateral = LateralContributionWindow( + study=self._study, + parent=self + ) lateral.show() def open_stricklers(self): diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index ade0eaffd326c3c07dc0ecfbe63afa2bacc871e7..a045a1fa60ab1cf46fc2d6ff51961c411c297371 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -178,12 +178,15 @@ class EdgeItem(QGraphicsItem): if self.graph.selected_item() == self: color = Qt.red elif self.graph.current_edge() == self: - color = Qt.black + color = Qt.blue elif not self.graph.graph.is_enable_edge(self.edge): color = Qt.darkGray - painter.setPen(QPen(color, 2, Qt.SolidLine, Qt.RoundCap, - Qt.RoundJoin)) + painter.setPen( + QPen( + color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin + ) + ) # Draw the line painter.drawLine(line) diff --git a/src/View/Reservoir/Edit/Plot.py b/src/View/Reservoir/Edit/Plot.py index 8f8f49b858228c34d3b143ec17bea797afa9f146..8c75a19971d85a4284fd4b234c4f76063721b636 100644 --- a/src/View/Reservoir/Edit/Plot.py +++ b/src/View/Reservoir/Edit/Plot.py @@ -69,7 +69,7 @@ class Plot(PamhyrPlot): ) # Plot label - #header = self.data.header + # header = self.data.header self.canvas.axes.set_xlabel( self._table_headers["z"], color='black', fontsize=10 ) diff --git a/src/View/Reservoir/Edit/Window.py b/src/View/Reservoir/Edit/Window.py index 34724bea63ab44e4e37aec5557fd02a32a2bd9f5..865ae6c89e1eb109bbf570a3afe9c937aa2c329e 100644 --- a/src/View/Reservoir/Edit/Window.py +++ b/src/View/Reservoir/Edit/Window.py @@ -88,18 +88,13 @@ class EditReservoirWindow(PamhyrWindow): def setup_table(self): headers = {} table_headers = self._trad.get_dict("table_headers") - #for h in self._data.header: - #headers[h] = table_headers[h] table = self.find(QTableView, "tableView") self._table = TableModel( table_view=table, table_headers=table_headers, editable_headers=table_headers, - #editable_headers=self._data.header, - delegates={ - #"time": self._delegate_time, - }, + delegates={}, data=self._data, undo=self._undo_stack, opt_data=self._study.time_system @@ -181,7 +176,7 @@ class EditReservoirWindow(PamhyrWindow): rows = self.index_selected_rows() table = [] - #table.append(self._data.header) + # table.append(self._data.header) table.append(self._trad.get_dict("table_headers")) data = self._data.data diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py index 5709efb4fa903d8616100decea36f2f62acf9e7e..f236bf8f3f7871004d56fe72a6202711eb11ef7e 100644 --- a/src/View/Results/PlotH.py +++ b/src/View/Results/PlotH.py @@ -92,31 +92,24 @@ class PlotH(PamhyrPlot): self.ts = list(self.results.get("timestamps")) self.ts.sort() - self.canvas.axes.set_xlim( - left=min(self.ts), right=max(self.ts) - ) - # Draw discharge for each timestamp x = self.ts y = profile.get_key("Q") - if len(self.ts) != len(x): - logger.warning( - "Results as less Q data ({len(x)}) " + - "than timestamps ({len(self.ts)}) " + - "for profile {self._current_profile_id}" - ) - return + self._line, = self.canvas.axes.plot( + x, y, lw=1., + color='r', + markersize=3, marker='+' + ) - self.canvas.axes.set_ylim( - [min(min(y), 0), max(y) + 10] + self._current, = self.canvas.axes.plot( + self._current_timestamp, + y[self.ts.index(self._current_timestamp)], + lw=1., color='b', + markersize=3, marker='+' ) - self._line, = self.canvas.axes.plot( - x, y, lw=1., - color='r', - markersize=3, marker='+' - ) + self.canvas.axes.relim() # Custom time display nb = len(x) @@ -175,12 +168,21 @@ class PlotH(PamhyrPlot): def set_timestamp(self, timestamp): self._current_timestamp = timestamp - #self.update() + self.update() def update(self): reach = self.results.river.reach(self._current_reach_id) profile = reach.profile(self._current_profile_id) + x = self.ts y = profile.get_key("Q") - self._line.set_data(x,y) + + self._line.set_data(x, y) + + self._current.set_data( + self._current_timestamp, + y[self.ts.index(self._current_timestamp)] + ) + + self.canvas.axes.relim() self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py index 93f91c9f4282527c8fb360d7f0566ba465a62f89..34ed91f9b28c1a4730dcb0b379579aa08ecbb22d 100644 --- a/src/View/Results/PlotKPC.py +++ b/src/View/Results/PlotKPC.py @@ -105,8 +105,14 @@ class PlotKPC(PamhyrPlot): ) self.profile, = self.canvas.axes.plot( - [kp[self._current_profile_id], kp[self._current_profile_id]], - [z_max[self._current_profile_id],z_min[self._current_profile_id]], + [ + kp[self._current_profile_id], + kp[self._current_profile_id] + ], + [ + z_max[self._current_profile_id], + z_min[self._current_profile_id] + ], color='red', lw=1. ) @@ -137,7 +143,13 @@ class PlotKPC(PamhyrPlot): z_min = reach.geometry.get_z_min() z_max = reach.geometry.get_z_max() self.profile.set_data( - [kp[self._current_profile_id], kp[self._current_profile_id]], - [z_max[self._current_profile_id],z_min[self._current_profile_id]] + [ + kp[self._current_profile_id], + kp[self._current_profile_id] + ], + [ + z_max[self._current_profile_id], + z_min[self._current_profile_id] + ] ) self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py index 895df719633913c52947433160ed2280e6bad16b..1db320f91c80c4bff5f786b677029be1fab020e3 100644 --- a/src/View/Results/PlotSedProfile.py +++ b/src/View/Results/PlotSedProfile.py @@ -51,13 +51,13 @@ class PlotSedProfile(PamhyrPlot): profiles_sl = list( map( lambda sl: sl[0], - profile.get_ts_key(self._current_timestamp, "sl") + profile.get_ts_key(self._current_timestamp, "sl")[0] ) ) profiles_sl_0 = list( map( lambda sl: sl[0], - profile.get_ts_key(0.0, "sl") + profile.get_ts_key(0.0, "sl")[0] ) ) diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py index 3978930d2fb265ed60da62ce5b91bde6e2ccd2a9..b7f229c8c113a4425e96b4174e4a4957cad63b97 100644 --- a/src/View/Results/PlotSedReach.py +++ b/src/View/Results/PlotSedReach.py @@ -124,14 +124,14 @@ class PlotSedReach(PamhyrPlot): profiles_sl_0 = list( map( # Get SL list for profile p at time 0 (initial data) - lambda p: p.get_ts_key(0.0, "sl"), + lambda p: p.get_ts_key(0.0, "sl")[0], reach.profiles ) ) profiles_sl = list( map( # Get SL list for profile p at current time - lambda p: p.get_ts_key(self._current_timestamp, "sl"), + lambda p: p.get_ts_key(self._current_timestamp, "sl")[0], reach.profiles ) ) @@ -163,7 +163,10 @@ class PlotSedReach(PamhyrPlot): z_sl = reduce( lambda acc, v: acc + [ list( - map(lambda x, y: y - x, v, acc[-1]) + map( + lambda x, y: y - x, + v, acc[-1] + ) ) ], sl_0, diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index 559fd9a6c05b4797a2cd59ffc110166416edd309..3d0e22810c68749e59835a45a45c1b42f581a76d 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -136,10 +136,14 @@ class PlotXY(PamhyrPlot): poly_x = [0] poly_y = [0] - self.fill = self.canvas.axes.fill(poly_x, poly_y, color='skyblue', alpha=0.7) + self.fill = self.canvas.axes.fill( + poly_x, poly_y, + color='skyblue', + alpha=0.7 + ) - #self.canvas.axes.autoscale_view(True, True, True) - #self.canvas.axes.autoscale() + # self.canvas.axes.autoscale_view(True, True, True) + # self.canvas.axes.autoscale() self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() if self.toolbar is not None: @@ -166,7 +170,7 @@ class PlotXY(PamhyrPlot): # Current profile profile = reach.profile(self._current_profile_id).geometry - self.plot_selected.set_data(profile.x(),profile.y()) + self.plot_selected.set_data(profile.x(), profile.y()) self.plot_selected.set_visible(True) self.canvas.draw_idle() @@ -196,12 +200,12 @@ class PlotXY(PamhyrPlot): poly_r_x.append(ptY.x) poly_r_y.append(ptY.y) - #self.canvas.axes.plot( - #x, y, lw=1., - #color='b', - #markersize=1, - #marker='o' - #) + # self.canvas.axes.plot( + # x, y, lw=1., + # color='b', + # markersize=1, + # marker='o' + # ) poly_x = poly_l_x + list(reversed(poly_r_x)) poly_y = poly_l_y + list(reversed(poly_r_y)) diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 75682ed557915fd4945b304e4e4e0e4b639b470a..75e95f2181feef09739553d0f8db434c98dcc2e9 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -97,7 +97,7 @@ class ResultsWindow(PamhyrWindow): self._additional_plot = {} self.setup_table() - self.setup_plot() + self.setup_plots() self.setup_slider() self.setup_statusbar() self.setup_connections() @@ -125,9 +125,13 @@ class ResultsWindow(PamhyrWindow): self._slider_time.setValue(len(self._timestamps) - 1) self._icon_start = QIcon() - self._icon_start.addPixmap(QPixmap('./src/View/ui/ressources/media-playback-start.png')) + self._icon_start.addPixmap( + QPixmap('./src/View/ui/ressources/media-playback-start.png') + ) self._icon_pause = QIcon() - self._icon_pause.addPixmap(QPixmap('./src/View/ui/ressources/media-playback-pause.png')) + self._icon_pause.addPixmap( + QPixmap('./src/View/ui/ressources/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") @@ -136,7 +140,7 @@ class ResultsWindow(PamhyrWindow): self._button_last = self.find(QPushButton, f"lastButton") self._timer = QTimer(self) - def setup_plot(self): + def setup_plots(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( @@ -392,7 +396,7 @@ class ResultsWindow(PamhyrWindow): self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) self.plot_kpc.set_timestamp(timestamp) - # self.plot_h.set_timestamp(timestamp) + self.plot_h.set_timestamp(timestamp) if self._study.river.has_sediment(): self.plot_sed_reach.set_timestamp(timestamp) diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 96ada16e0053d08a4bc0cd72621dc3f7c3432504..6067f5e12b9e21011b897dec7bdb13c151d7e710 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -74,10 +74,16 @@ class SelectSolverWindow(PamhyrDialog): self.setup_combobox() self.setup_connections() + self.select_last_solver() def setup_combobox(self): solvers = self._config.solvers - solvers_name = list(map(lambda s: s.name + f" - ({s._type})", solvers)) + solvers_name = list( + map( + self._format_solver_name, + solvers + ) + ) self.combobox_add_items("comboBox", solvers_name) @@ -86,6 +92,26 @@ class SelectSolverWindow(PamhyrDialog): self.find(QPushButton, "pushButton_cancel")\ .clicked.connect(self.reject) + def select_last_solver(self): + solvers = self._config.solvers + last = self._config.last_solver_name + + solver = list( + filter( + lambda s: s.name == last, + solvers + ) + ) + + if len(solver) != 0: + self.set_combobox_text( + "comboBox", + self._format_solver_name(solver[0]) + ) + + def _format_solver_name(self, solver): + return f"{solver.name} - ({solver._type})" + @property def solver(self): return self._solver @@ -94,6 +120,8 @@ class SelectSolverWindow(PamhyrDialog): solver_name = self.get_combobox_text("comboBox") solver_name = solver_name.rsplit(" - ", 1)[0] + self._config.update_last_solver_used(solver_name) + self._solver = next( filter( lambda s: s.name == solver_name, @@ -124,40 +152,11 @@ class SolverLogWindow(PamhyrWindow): self.setup_action() self.setup_alarm() self.setup_connections() + self.setup_workdir() + self.setup_process() - self._workdir = "" - if self._study.filename == "": - self._workdir = tempfile.TemporaryDirectory() - else: - self._workdir = os.path.join( - os.path.dirname(self._study.filename), - "_PAMHYR_", - self._study.name.replace(" ", "_"), - self._solver.name.replace(" ", "_"), - ) - os.makedirs(self._workdir, exist_ok=True) - - self._alarm.start(500) - self._output = Queue() - self._process = self.new_process(parent) - - self._log(f" *** Export study {self._solver.name}", color="blue") - self._solver.export(self._study, self._workdir, qlog=self._output) - - self.update() - - self._log(f" *** Run solver {self._solver.name}", color="blue") - self._solver.run( - study, - process=self._process, - output_queue=self._output - ) - - def new_process(self, parent): - new = QProcess(parent) - new.setWorkingDirectory(self._workdir) - new.setProcessChannelMode(QProcess.MergedChannels) - return new + self.export() + self.run() def setup_action(self): self.find(QAction, "action_start").setEnabled(False) @@ -182,30 +181,74 @@ class SolverLogWindow(PamhyrWindow): self._alarm.timeout.connect(self.update) + def setup_workdir(self): + self._workdir = "" + if self._study.filename == "": + self._workdir = tempfile.TemporaryDirectory() + else: + self._workdir = os.path.join( + os.path.dirname(self._study.filename), + "_PAMHYR_", + self._study.name.replace(" ", "_"), + self._solver.name.replace(" ", "_"), + ) + os.makedirs(self._workdir, exist_ok=True) + + def setup_process(self): + self._alarm.start(500) + self._output = Queue() + self._process = self.new_process(self._parent) + + def new_process(self, parent): + new = QProcess(parent) + new.setWorkingDirectory(self._workdir) + new.setProcessChannelMode(QProcess.MergedChannels) + return new + + def export(self): + self._log(f" *** Export study {self._solver.name}", color="blue") + self._solver.export(self._study, self._workdir, qlog=self._output) + self.update() + + ####### + # LOG # + ####### + def _log(self, msg, color=None): if type(msg) is str: - logger.info(f"solver: {msg}") + self._log_str(msg, color) + elif type(msg) is int: + self._log_int(msg, color) - msg = msg.rsplit('\n')[0] + def _log_str(self, msg, color=None): + logger.info(f"solver: {msg}") + msg = msg.rsplit('\n')[0] - if color is not None: - msg = f"<font color=\"{color}\">" + msg + "</font>" + if color is not None: + msg = f"<font color=\"{color}\">" + msg + "</font>" - self.find(QTextEdit, "textEdit").append(msg) - elif type(msg) is int: - logger.info(f"solver: Returns {msg}") + self.find(QTextEdit, "textEdit").append(msg) - color = "blue" if msg == 0 else "red" - self.find(QTextEdit, "textEdit")\ - .append(f"<font color=\"{color}\">" + - f" *** Finished with code {msg}" + - "</font>") + def _log_int(self, int_code, color=None): + logger.info(f"solver: Returns {int_code}") + color = "blue" if int_code == 0 else "red" - self.statusbar.showMessage( - "Done" if msg == 0 else "Failed", - 3000 + self.find(QTextEdit, "textEdit")\ + .append( + f"<font color=\"{color}\">" + + f" *** Finished with code {int_code}" + + "</font>" ) + self.statusbar.showMessage( + "Done" if int_code == 0 else "Failed", + 3000 + ) + + ########## + # UPDATE # + ########## + def update(self): if self._solver.is_stoped(): self.find(QAction, "action_start").setEnabled(True) @@ -230,11 +273,24 @@ class SolverLogWindow(PamhyrWindow): def _update_logs_all(self): while self._output.qsize() != 0: s = self._output.get() + if type(s) is str and "[ERROR]" in s: self._log(s, color="red") else: self._log(s) + #################### + # Process controle # + #################### + + def run(self): + self._log(f" *** Run solver {self._solver.name}", color="blue") + self._solver.run( + self._study, + process=self._process, + output_queue=self._output + ) + def start(self): if self._solver.is_stoped(): self._log(f" *** Export study {self._solver.name}", color="blue") @@ -276,6 +332,10 @@ class SolverLogWindow(PamhyrWindow): if self._solver.log_file() != "": self.find(QAction, "action_log_file").setEnabled(True) + ########### + # Results # + ########### + def results(self): if self._results is None: self._results = self._solver.results( @@ -284,6 +344,8 @@ class SolverLogWindow(PamhyrWindow): self._parent.set_results(self._solver, self._results) self._parent.open_solver_results(self._solver, self._results) + self._solver.has_results_loaded() + def log_file(self): file_name = os.path.join(self._workdir, self._solver.log_file()) log = SolverLogFileWindow( diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 642fdc5760742eb3400c9ed818494249952e587e..5521402c2d6c9ccdec013bfaae088ceb53a03cba 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -129,4 +129,7 @@ class ParamTranslate(PamhyrTranslate): "mage_min_reach_volume_to_check": _translate("SolverParameters", "Minimum reach volume to check"), + "mage_init_internal": + _translate("SolverParameters", + "Use Mage internal initialization (Y/N)"), } diff --git a/src/View/Study/Window.py b/src/View/Study/Window.py index e8b9c7f83791ae14e05671d78958a78771669461..bfb615d1ecb99fe853ffbc9f5565c04d38e9a55e 100644 --- a/src/View/Study/Window.py +++ b/src/View/Study/Window.py @@ -101,4 +101,5 @@ class NewStudyWindow(PamhyrDialog): self._study.use_date(date) else: self._study.use_time() + self.done(True) diff --git a/src/View/Tools/ASubWindow.py b/src/View/Tools/ASubWindow.py index 8530c15c9e5849053c51ab0166fa9cec5b8fffa2..7ef5f964b341ec5b229ea179f48d441892bf1485 100644 --- a/src/View/Tools/ASubWindow.py +++ b/src/View/Tools/ASubWindow.py @@ -81,7 +81,7 @@ class WindowToolKit(object): header = row.copy() continue - values.append(list(filter(lambda s: s != '', row))) + values.append(row) return header, values diff --git a/src/View/Tools/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py index 7fe21bb7bfab7cf0b1a40d3b5fac61ca8fff8792..d0b833b8efa46a7179f86e746b66c6bcc0c1f98b 100644 --- a/src/View/Tools/ListedSubWindow.py +++ b/src/View/Tools/ListedSubWindow.py @@ -55,11 +55,12 @@ class ListedSubWindow(object): logger.info(f"Close window: ({h}) {self.sub_win_cnt}") def _sub_win_exists(self, h): - return reduce( + res = reduce( lambda acc, el: (acc or (h == (el[1].hash()))), self.sub_win_list, False ) + return res def sub_win_exists(self, h): return self._sub_win_exists(h) diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index b77e26b9ceab0afe487e1b83caaf28fd1a5af4dd..803e58421fd97a95bf68a03183df2e4e5281b9c8 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -83,7 +83,8 @@ class PamhyrTableModel(QAbstractTableModel): trad=None, data=None, undo=None, - opt_data=None): + opt_data=None, + parent=None): super(PamhyrTableModel, self).__init__() self._table_view = table_view @@ -93,6 +94,7 @@ class PamhyrTableModel(QAbstractTableModel): self._editable_headers = editable_headers self._delegates = delegates self._trad = trad + self._parent = parent self._data = data self._opt_data = opt_data @@ -119,8 +121,7 @@ class PamhyrTableModel(QAbstractTableModel): options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if (self._editable_headers or - self._headers[column] in self._editable_headers): + if self._headers[column] in self._editable_headers: options |= Qt.ItemIsEditable return options diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py index 74756167b65b01ebefb02a9f996095d28ced6923..87829b8dad058c0933a2285c5f2a273d950bf5e7 100644 --- a/src/View/Tools/PamhyrWindow.py +++ b/src/View/Tools/PamhyrWindow.py @@ -186,3 +186,8 @@ class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools): self._hash_data.append(self._config) self._set_title() + + def done(self, result): + if self.parent is not None: + self.parent.sub_win_del(self.hash()) + super(PamhyrDialog, self).done(result) diff --git a/src/View/ui/BasicHydraulicStructures.ui b/src/View/ui/BasicHydraulicStructures.ui new file mode 100644 index 0000000000000000000000000000000000000000..add59b7c8c5d4de7cdde19a8eaec49f1789c0e3a --- /dev/null +++ b/src/View/ui/BasicHydraulicStructures.ui @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>Basic hydraulic structures</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QSplitter" name="splitter_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QWidget" name="verticalLayoutWidget_3"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTableView" name="tableView"> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <item> + <widget class="QCheckBox" name="checkBox"> + <property name="text"> + <string>Enable / Disable basic hydraulic structure</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="QWidget" name="verticalLayoutWidget"> + <layout class="QVBoxLayout" name="verticalLayout"/> + </widget> + <widget class="QWidget" name="verticalLayoutWidget_2"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTableView" name="tableView_2"/> + </item> + </layout> + </widget> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>22</height> + </rect> + </property> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <widget class="QToolBar" name="toolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="action_add"/> + <addaction name="action_delete"/> + </widget> + <action name="action_add"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset> + </property> + <property name="text"> + <string>Add</string> + </property> + <property name="toolTip"> + <string>Add a new point</string> + </property> + </action> + <action name="action_delete"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset> + </property> + <property name="text"> + <string>Delete</string> + </property> + <property name="toolTip"> + <string>Delete points</string> + </property> + </action> + <action name="action_edit"> + <property name="icon"> + <iconset> + <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset> + </property> + <property name="text"> + <string>Edit</string> + </property> + <property name="toolTip"> + <string>Edit selected hydraulic structure</string> + </property> + </action> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/View/ui/GeometryReach.ui b/src/View/ui/GeometryReach.ui index a11a2279caabc72fba0a586e430b2d86a9a60899..d18d0e14e34cb335b9b9f5ae31a99e023241bcb8 100644 --- a/src/View/ui/GeometryReach.ui +++ b/src/View/ui/GeometryReach.ui @@ -6,7 +6,7 @@ <rect> <x>0</x> <y>0</y> - <width>868</width> + <width>1280</width> <height>720</height> </rect> </property> @@ -101,7 +101,7 @@ <rect> <x>0</x> <y>0</y> - <width>868</width> + <width>1280</width> <height>22</height> </rect> </property> diff --git a/src/View/ui/HydraulicStructures.ui b/src/View/ui/HydraulicStructures.ui new file mode 100644 index 0000000000000000000000000000000000000000..813da5b5f48ef45a2e14e4c473eb3263ed35f1b4 --- /dev/null +++ b/src/View/ui/HydraulicStructures.ui @@ -0,0 +1,139 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>Hydraulic structures</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QSplitter" name="splitter_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QWidget" name="verticalLayoutWidget_3"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTableView" name="tableView"> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <item> + <widget class="QCheckBox" name="checkBox"> + <property name="text"> + <string>Enable / Disable hydraulic structure</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="QWidget" name="verticalLayoutWidget"> + <layout class="QVBoxLayout" name="verticalLayout"/> + </widget> + <widget class="QWidget" name="verticalLayoutWidget_2"> + <layout class="QVBoxLayout" name="verticalLayout_2"/> + </widget> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>22</height> + </rect> + </property> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <widget class="QToolBar" name="toolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="action_add"/> + <addaction name="action_delete"/> + <addaction name="action_edit"/> + </widget> + <action name="action_add"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset> + </property> + <property name="text"> + <string>Add</string> + </property> + <property name="toolTip"> + <string>Add a new point</string> + </property> + </action> + <action name="action_delete"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset> + </property> + <property name="text"> + <string>Delete</string> + </property> + <property name="toolTip"> + <string>Delete points</string> + </property> + </action> + <action name="action_edit"> + <property name="icon"> + <iconset> + <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset> + </property> + <property name="text"> + <string>Edit</string> + </property> + <property name="toolTip"> + <string>Edit selected hydraulic structure</string> + </property> + </action> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index e4d00954ccfff566746cfb1c98d94be09c52d6b1..bf73c3d3cb232511420f01a2fd72ad73438b235e 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -132,6 +132,7 @@ <addaction name="action_menu_edit_friction"/> <addaction name="action_menu_edit_lateral_contribution"/> <addaction name="action_menu_edit_reservoirs"/> + <addaction name="action_menu_edit_hydraulic_structures"/> </widget> <widget class="QMenu" name="menu_results"> <property name="title"> @@ -934,6 +935,14 @@ <string>Edit reservoirs</string> </property> </action> + <action name="action_menu_edit_hydraulic_structures"> + <property name="text"> + <string>Hydraulic structures</string> + </property> + <property name="toolTip"> + <string>Edit hydraulic structures</string> + </property> + </action> </widget> <resources/> <connections> diff --git a/src/config.py b/src/config.py index c8613a42323b307d0a0395d698644ca0b9d3b905..e5c314c3b96a0654507d0e640433c9be66a2cba7 100644 --- a/src/config.py +++ b/src/config.py @@ -35,7 +35,7 @@ logger = logging.getLogger() class Config(SQL): def __init__(self): - self._version = '0.0.3' + self._version = '0.0.4' self.filename = Config.filename() self.set_default_value() @@ -113,11 +113,17 @@ class Config(SQL): '' ) """) + if int(release) < 3: self.execute(f"INSERT INTO data VALUES ('last_study', '')") self.execute( f"INSERT INTO data VALUES ('close_correctly', 'True')") + if int(release) < 4: + self.execute( + f"INSERT INTO data VALUES ('last_solver_name', '')" + ) + self.commit() def _load_solver(self): @@ -195,6 +201,10 @@ class Config(SQL): v = self.execute("SELECT value FROM data WHERE key='close_correctly'") self.close_correctly = v[0] == "True" + # Last Solver + v = self.execute("SELECT value FROM data WHERE key='last_solver_name'") + self.last_solver_name = v[0] + # Debug v = self.execute("SELECT value FROM data WHERE key='debug'") self.debug = v[0] == "True" @@ -250,6 +260,7 @@ class Config(SQL): "last_study": self.last_study, "close_correctly": self.close_correctly, "debug": self.debug, + "last_solver_name": self.last_solver_name, } for key in data: @@ -306,6 +317,9 @@ class Config(SQL): self.last_study = "" self.close_correctly = False + # Last Solver + self.last_solver_name = "" + # Debug self.debug = False @@ -332,6 +346,14 @@ class Config(SQL): ) self.commit() + def update_last_solver_used(self, solver_name): + self.last_solver_name = solver_name + self.execute( + "UPDATE data SET " + + f"value='{self._db_format(self.last_solver_name)}' " + + "WHERE key='last_solver_name'" + ) + @classmethod def filename(cls): file = "" diff --git a/src/tools.py b/src/tools.py index b6f78857a90de16178ce21d680047c6015e70b99..1ad8c382ec46c63e5881573f43eafe014bfbc30c 100644 --- a/src/tools.py +++ b/src/tools.py @@ -67,6 +67,7 @@ def logger_color_reset(): return f"{Style.RESET_ALL}" return "" + def logger_exception(exception): logger.error( f"[{Fore.RED}ERROR{Style.RESET_ALL}] " + diff --git a/tests.sh b/tests.sh index 06c8fd846e45c5ffa156fde542901a4387b12e23..ff235027a698f756112e4ffbdf28d8a90505a863 100755 --- a/tests.sh +++ b/tests.sh @@ -16,7 +16,7 @@ cd .. echo " PEP8" -pycodestyle ./src +pycodestyle --exclude="*_to_*.py" ./src if [ $? -eq 0 ] then