From 6c9481296b2aa8c48ccdc0719cc7f26e9cad624a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 20 Sep 2023 15:21:46 +0200 Subject: [PATCH 001/248] doc: dev: Add SQL section with examples. --- doc/dev/documentation.org | 321 +++++++++++++++++++++++++++++--------- 1 file changed, 245 insertions(+), 76 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index e245b959..56b45032 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -139,101 +139,270 @@ https://doc.qt.io/qt-5/model-view-programming.html ** TODO Model -- Model de donnée Python -- Correspond à une sauvegarde SQL +The model is a set of Python classes. In Pamhyr2, this classes must +respect some constraint. Each model class must inherits +=Model.Tools.SQLSubModel= abstract class, except the =Model.Study= +class who inherits =Model.Tools.SQLModel= (see [[SQL]]). + +The model entry point is the Study class. It contains infomation about +the study: their name, description, time system, and so on. Their +contains a River object too. This river object inherits the network +graph and contains a list of =RiverNode= and a list of =RiverReach= +(an edge who contains a source node, and destination node). +=RiverReach= contrains geometry, so, the river network (node and edge) +associated with the geometry forms the basis of the model, and the +other components are linked to one of these basic components. #+name: graph-model #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies", float="t") +#+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies (A -> B means A can contain references to B)", float="t") #+begin_src dot :file "images/graph-model.png" :cache no digraph { - bgcolor="transparent"; - node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; - study[label="Study"]; - river[label="River"]; + subgraph cluster0 { + style=dashed; + study[label="Study"]; + river[label="River"]; - subgraph cluster00 { - label="Network" - rnode[label="RiverNode"]; - redge[label="RiverReach"]; - } + subgraph cluster00 { + style=solid; + label="Network" + rnode[label="RiverNode"]; + redge[label="RiverReach"]; + } - frictionlist[label="FrictionList"]; + subgraph cluster06 { + style=solid; + label="Greometry" + georeach[label="Reach"]; + geocrosssection[label="Cross-section"]; + geopoint[label="Point"]; + } + } - subgraph cluster01 { - label="Stricklers"; - stricklers[label="Stricklers"]; - stricklerslist[label="StricklersList"]; - } - subgraph cluster02 { - label="BoundaryCondition"; - boundaryconditionlist[label="BoundaryConditionList"]; - boundarycondition[label="BoundaryCondition"]; - } - subgraph cluster03 { - label="LateralContribution"; - lateralcontributionlist[label="LateralContributionList"]; - lateralcontribution[label="LateralContribution"]; - } - subgraph cluster04 { - label="InitialConditions"; - initialconditionsdict[label="InitialConditionsDict"]; - initialconditions[label="InitialConditions"]; - } + //subgraph cluster1 { + // style=dashed; + frictionlist[label="FrictionList"]; - solverparameterslist[label="SolverParametersList"]; + subgraph cluster01 { + style=solid; + label="Stricklers"; + stricklers[label="Stricklers"]; + stricklerslist[label="StricklersList"]; + } + subgraph cluster02 { + style=solid; + label="BoundaryCondition"; + boundaryconditionlist[label="BoundaryConditionList"]; + boundarycondition[label="BoundaryCondition"]; + } + subgraph cluster03 { + style=solid; + label="LateralContribution"; + lateralcontributionlist[label="LateralContributionList"]; + lateralcontribution[label="LateralContribution"]; + } + subgraph cluster04 { + style=solid; + label="InitialConditions"; + initialconditionsdict[label="InitialConditionsDict"]; + initialconditions[label="InitialConditions"]; + } - subgraph cluster05 { - label="Sediment"; - sedimentlayerlist[label="SedimentLayerList"]; - sedimentlayer[label="SedimentLayer"]; - layer[label="Layer"]; - } + solverparameterslist[label="SolverParametersList"]; - subgraph cluster06 { - label="Greometry" - georeach[label="Reach"]; - geocrosssection[label="Cross-section"]; - geopoint[label="Point"]; - } + subgraph cluster05 { + style=solid; + label="Sediment"; + sedimentlayerlist[label="SedimentLayerList"]; + sedimentlayer[label="SedimentLayer"]; + layer[label="Layer"]; + } + //} - subgraph cluster07 { - label="Results" - results[label="Results"] - rriver[label="River"]; - rreach[label="Reach"]; - rcrosssection[label="Cross-section"]; - } + subgraph cluster2 { + style=dashed; + label="Results" + results[label="Results"] + rriver[label="River"]; + rreach[label="Reach"]; + rcrosssection[label="Cross-section"]; + } - study -> river; - river -> rnode; - river -> redge; - redge -> rnode; - river -> boundaryconditionlist -> boundarycondition -> rnode; - river -> lateralcontributionlist -> lateralcontribution -> redge; - river -> initialconditionsdict -> initialconditions; - initialconditions -> redge; - river -> stricklerslist -> stricklers; - river -> solverparameterslist; - river -> sedimentlayerlist -> sedimentlayer -> layer; - redge -> frictionlist -> stricklers; - redge -> georeach -> geocrosssection -> geopoint; - geocrosssection -> sedimentlayer; - geopoint -> sedimentlayer; + study -> river; + river -> rnode; + river -> redge; + redge -> rnode; + river -> boundaryconditionlist -> boundarycondition -> rnode; + river -> lateralcontributionlist -> lateralcontribution -> redge; + river -> initialconditionsdict -> initialconditions; + initialconditions -> redge; + river -> stricklerslist -> stricklers; + river -> solverparameterslist; + river -> sedimentlayerlist -> sedimentlayer -> layer; + redge -> frictionlist -> stricklers; + redge -> georeach -> geocrosssection -> geopoint; + geocrosssection -> sedimentlayer; + geopoint -> sedimentlayer; - results -> study[style="dashed"]; - results -> rriver; - rriver -> river[style="dashed"]; - rriver -> rreach; - rreach -> georeach[style="dashed"]; - rreach -> rcrosssection; - rcrosssection -> geocrosssection[style="dashed"]; + results -> study; + results -> rriver; + rriver -> river; + rriver -> rreach; + rreach -> georeach; + rreach -> rcrosssection; + rcrosssection -> geocrosssection; } #+end_src -*** TODO SQL study file +*** TODO SQL + +The model must be export to a database file to create a study save +file. This file use SQLite3[fn:sqlite] format and the extention +=.pamhyr=. So, each model componante must be register into this study +file. To create, update, set and get information into SQLite database +we use SQL command. The database use version number and some +modification could be perform to update database. For each model +componante, correspond one or more SQL table to store information. To +normalize the interaction with database we made two classes, SQLModel +and SQLSubModel. The Study class use SQLModel because is the top of +the model hierachy. The rest of model class inherits to SQLSubModel. + +A class who inherits SQLSubModel, must implement some methods: +- =_sql_create=: Class method to create the database scheme +- =_sql_update=: Class method to update the database scheme if necessary +- =_sql_load=: Class method to load data from DB +- =_sql_save=: Method to save current object into DB + +Class method take in arguments: The class (=cls=), a function to +execute SQL command into the database (=execute=). In addition, the +update method take the previous version of database, load method take +an optional arguments =data= if additional infomation ar needed, and +who can contains whatever you want. The method save take in arguments +the current object (=self=), a function to execute SQL command into +the database (=execute=), and optional data (=data=). + +The class who inherits SQLSubModel can also define an class attribute +=_sub_classes= to set a formal class dependencies into database. This +attribute is use at database creation to create all table, and at +update to update all the database table. Let see examples of +SQLSubModel usage for two classes Foo and Bar with Foo contains list +of Bar (Listing [[sql-bar]] and [[sql-foo]]). + +#+NAME: sql-bar +#+CAPTION: Exemple of class Bar inherits SQLSubModel. +#+begin_src python :python python3 :results output :noweb yes + from Model.Tools.PamhyrDB import SQLSubModel + + class Bar(SQLSubModel): + _id_cnt = 0 + def __init__(self, id = -1, x = 0, y = 0): + self._x = x + self._y = y + if id == -1: + self.id = Bar._id_cnt + 1 + else: + self.id = id + Bar._id_cnt = max(id, Bar._id_cnt+1) + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE bar ( + id INTEGER NOT NULL PRIMARY KEY, + x INTEGER NOT NULL, + foo_id INTEGER NOT NULL, + FOREIGN KEY(foo_id) REFERENCES foo(id), + )""") + return True + + @classmethod + def _sql_update(cls, execute, version): + # If version is lesser than 0.0.2, add column to bar table + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + execute("ALTER TABLE bar ADD COLUMN y INTEGER") + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + table = execute( + f"SELECT id, x, y FROM bar WHERE foo_id = {data['id']}" + ) + for row in table: + bar = cls( + id = row[0], x = row[1], y = row[2], + ) + new.append(bar) + return new + + def _sql_save(self, execute, data = None): + execute("INSERT INTO bar (id,x,y,foo_id) VALUES " + + f"({self.id}, {self._x}, {self._y}, {data['id']})") +#+end_src + +#+NAME: sql-foo +#+CAPTION: Exemple of class Foo inherits SQLSubModel. +#+begin_src python :python python3 :results output :noweb yes + class Foo(SQLSubModel): + _id_cnt = 0 + _sub_classes = [Bar] + + def __init__(self, id = -1, name = ""): + self._name = name + self._bar = [] + # ... + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE foo ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + ) + """) + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, excute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + table = execute( + "SELECT id, name FROM foo" + ) + for row in table: + foo = cls( + id = row[0], + name = row[1], + ) + data = { + "id": row[0], # Current Foo ID + } + foo._bar = Bar._sql_load(execute, data=data) + new.append(foo) + return new + + def _sql_save(self, execute, data = None): + execute(f"DELETE FROM foo WHERE id = {self.id}") + execute(f"DELETE FROM bar WHERE foo_id = {self.id}") + # Save new data + execute(f"INSERT INTO bar (id,name) VALUES ({self.id}, {self._name})") + data = {"id": self.id} + for bar in self._bar: + bar._sql_save(execute, data=data) +#+end_src + +[fn:sqlite] The SQLite web site: https://www.sqlite.org/index.html +(last access 2023-09-20) + *** TODO List class *** TODO Dict class ** TODO View From 42dfdcc3b1fcc67a4a893f935779e3caaaa1650d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 20 Sep 2023 15:32:04 +0200 Subject: [PATCH 002/248] doc: dev: Complete Model section. --- doc/dev/documentation.org | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 56b45032..0279790c 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -137,7 +137,7 @@ in {{{file(src/Solver)}}} (see section [[Solver]]). [fn:qt-mv] The Qt Model/View documentation web page: https://doc.qt.io/qt-5/model-view-programming.html -** TODO Model +** Model The model is a set of Python classes. In Pamhyr2, this classes must respect some constraint. Each model class must inherits @@ -258,7 +258,7 @@ other components are linked to one of these basic components. } #+end_src -*** TODO SQL +*** SQL The model must be export to a database file to create a study save file. This file use SQLite3[fn:sqlite] format and the extention @@ -403,8 +403,22 @@ of Bar (Listing [[sql-bar]] and [[sql-foo]]). [fn:sqlite] The SQLite web site: https://www.sqlite.org/index.html (last access 2023-09-20) -*** TODO List class -*** TODO Dict class +*** List class + +A abstract class PamhyrModelList is available and provide some of +basic methods for object list in Model. This abstract class implement +some classic action in View like: insert new object, delete object, +sort, move object up, move object down, and so on. An variant exists +for multiple list with same type of object, each sublist is called +tab, because in View, this kind of list si prensented in different +table PamhyrModelListWithTab. + +*** Dict class + +A abstract class PamhyrModelDict is available and provide some of +basic methods for object dictionary in Model. This class is like +PamhyrModelList but use a dictionary instead of list. + ** TODO View *** TODO UI file *** TODO Translate From afa21c90576d882166668bd70b4a1b9d3d2add52 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 20 Sep 2023 16:46:37 +0200 Subject: [PATCH 003/248] doc: dev: View section (UI, window). --- doc/dev/documentation.org | 118 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 0279790c..16d6a69c 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -347,7 +347,7 @@ of Bar (Listing [[sql-bar]] and [[sql-foo]]). #+end_src #+NAME: sql-foo -#+CAPTION: Exemple of class Foo inherits SQLSubModel. +#+CAPTION: Exemple of class Foo inherits SQLSubModel and contains a list of Bar object (Listing [[sql-bar]]). #+begin_src python :python python3 :results output :noweb yes class Foo(SQLSubModel): _id_cnt = 0 @@ -420,11 +420,119 @@ basic methods for object dictionary in Model. This class is like PamhyrModelList but use a dictionary instead of list. ** TODO View -*** TODO UI file -*** TODO Translate -*** TODO Window + +Pamhyr2 use Qt as graphical user interface library with the +application "Qt designer" for windows or widget creation (see [[UI file]]) +and "Qt linguist" for interface translate (see [[Translate]]). In +addition, we use matplotlib as ploting library (see [[Plot]]). + +Typically, each model componant have an associated window in +application to add, delete or edit this componant. At top level of +View directory we found the =MainWindow.py= file and some +sub-directories. A view sub-directory contains: A =Window.py= file, a +=Table.py= file with table model definition if nessessary, one or more +=Plot*.py= file with plot class definition, a =translate.py= file with +componant translate, and possible other files or sub-directories. + +*** UI file + +We define as possible all Pamhyr2 windows and custom widgets with "Qt +designer". This application generate UI file who describes interface +organisation with table, layout, button, etc. This method is faster +than hand made windows and widget creation, and saves us some purely +descriptive code. The UI files are saved into =src/View/ui= for +window, and =/src/View/ui/Widgets= for custom widget. + +*** Window + +An abstract class ASubMainWindow is used for most of Pamhyr2 +windows. This class implemente some useful method allows to easy load +an window UI file. In addition, the abstract ListedSubWindow must be +also inherits by a window class, it is allow to keep a list of open +windows and avoid window duplicate. + +#+NAME: window +#+CAPTION: Example of Pamhyr2 window +#+begin_src python :python python3 :results output :noweb yes + class MyWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="My window", + study=None, config=None, parent=None): + self._study = study + self._config = config + self._parent = parent + + self._title = title + " - " + study.name + + super(MyWindow, self).__init__( + name=title, # The windows name for windows + # registration (avoid duplicate) + ui="My", # The ui file name (here "My.ui") + parent=parent # The parent window + ) + # Set Windows title + self.ui.setWindowTitle(self._title) + + self.setup_table() + self.setup_connections() + + def setup_table(self): + # Init table(s)... + + def setup_connections(self): + # Init action connection(s)... + + # ... +#+end_src + +Typically we called method =setup_*=, the method to initialize some +window componants or connections. + +*** Table + +An abstract class PamhyrTableModel is available to define a simple +QAbstractTableModel shortly. In simple cases, there are only =data= +and =setData= methode to implement, but the constructor needs more +information than a classic QAbstractTableModel class. + +#+begin_src python :python python3 :results output :noweb yes + # Table headers with display name translatable (translate.py) + table_headers = { + "foo": _translate("My", "Foo"), + "bar": _translate("My", "Bar"), + "baz": _translate("My", "Baz"), + } + + def retranslate(): + # Hack because dict definition is not able to be + # translate... (need to be called in window '__init__') + table_headers["foo"] = _translate("My", "Foo") + table_headers["bar"] = _translate("My", "Bar") + table_headers["baz"] = _translate("My", "Baz") + + # Table model definition (Table.py) + class MyTableModel(PamhyrTableModel): + def data(self, index, role): + # Retrun data at INDEX... + + @pyqtSlot() + def setData(self, index, value, role=Qt.EditRole): + # Set VALUE at INDEX... + + # Table model creation (Window.py) + self._model = MyTableModel( + table_view = table, # The table view object + table_headers = table_headers, # The table column headers dict + # (with traduction) + editable_headers = ["foo", "bar"], # List of editable column name + delegates = { + "bar": self.my_delegate, # Custom delegate for column 'bar' + }, + data = self._my_lst, # The data + undo = self._undo_stack, # The undo command stack + ) +#+end_src + *** TODO UndoCommand -*** TODO Table *** TODO Plot *** TODO Mainwindow ** TODO Solver From edc5b65234ad64a6cc123b94c68bab8a138248bf Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:03:07 +0200 Subject: [PATCH 004/248] Scripts: Convert plot_3DST.py to Pamhyr2 script. --- src/Model/Geometry/Reach.py | 2 +- src/Scripts/Hello.py | 2 +- src/Scripts/P3DST.py | 115 ++++++++++++++++++++++++++++++++++++ src/Scripts/plot_3DST.py | 68 --------------------- src/pamhyr.py | 2 + 5 files changed, 119 insertions(+), 70 deletions(-) create mode 100644 src/Scripts/P3DST.py delete mode 100644 src/Scripts/plot_3DST.py diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 0c641478..26cdefc9 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -42,7 +42,7 @@ class Reach(SQLSubModel): ] def __init__(self, status=None, parent=None): - self.id = parent.id + self.id = parent.id if parent is not None else 0 self._status = status self._parent = parent self._profiles: List[Profile] = [] diff --git a/src/Scripts/Hello.py b/src/Scripts/Hello.py index b1c27fe5..c7333b84 100644 --- a/src/Scripts/Hello.py +++ b/src/Scripts/Hello.py @@ -1,4 +1,4 @@ -# help.py -- Pamhyr +# Hello.py -- Pamhyr # Copyright (C) 2023 INRAE # # This program is free software: you can redistribute it and/or modify diff --git a/src/Scripts/P3DST.py b/src/Scripts/P3DST.py new file mode 100644 index 00000000..55a99928 --- /dev/null +++ b/src/Scripts/P3DST.py @@ -0,0 +1,115 @@ +# P3DST.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 . + +# -*- coding: utf-8 -*- + +import sys +import logging + +from matplotlib import pyplot as plt +from numpy import mean + +from Scripts.AScript import AScript + +from Model.Saved import SavedStatus +from Model.Geometry.Reach import Reach + +logger = logging.getLogger() + +class Script3DST(AScript): + name = "3DST" + description = "Display a 3D plot of a river reach from ST file" + + def usage(self): + logger.info(f"Usage : {self._args[0]} 3DST ") + + def set_axes_equal(self, ax): + """Make axes of 3D plot have equal scale + + Make axes of 3D plot have equal scale so that spheres appear + as spheres, cubes as cubes, etc.. This is one possible + solution to Matplotlib's ax.set_aspect('equal') and + ax.axis('equal') not working for 3D. + + Args: + ax: a matplotlib axis, e.g., as output from plt.gca() + + Returns: + The input axis + """ + x_limits = ax.get_xlim3d() + y_limits = ax.get_ylim3d() + z_limits = ax.get_zlim3d() + + x_range = abs(x_limits[1] - x_limits[0]) + x_middle = mean(x_limits) + y_range = abs(y_limits[1] - y_limits[0]) + y_middle = mean(y_limits) + z_range = abs(z_limits[1] - z_limits[0]) + z_middle = mean(z_limits) + + # The plot bounding box is a sphere in the sense of the infinity + # norm, hence I call half the max range the plot radius. + plot_radius = 0.5*max([x_range, y_range, z_range]) + + ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) + ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) + ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) + + return ax + + def run(self): + try: + st_file = self._args[2] + logger.info(f"Use ST file: {st_file}") + except Exception as e: + logger.error(f"Arguments parcing: {str(e)}") + return 1 + + try: + status = SavedStatus() + + my_reach = Reach(status = status) + my_reach.import_geometry(st_file) + my_reach.compute_guidelines() + + ax = plt.figure().add_subplot(projection="3d") + logger.info(my_reach.get_x()) + for x, y, z in zip( + my_reach.get_x(), + my_reach.get_y(), + my_reach.get_z() + ): + ax.plot(x, y, z, color='r', lw=1.) + + for x, y, z in zip( + my_reach.get_guidelines_x(), + my_reach.get_guidelines_y(), + my_reach.get_guidelines_z() + ): + ax.plot(x, y, z, color='b', lw=1.) + + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_zlabel('Z') + plt.tight_layout() + self.set_axes_equal(ax) + plt.show() + + return 0 + except Exception as e: + logger.error(str(e)) + return 1 diff --git a/src/Scripts/plot_3DST.py b/src/Scripts/plot_3DST.py deleted file mode 100644 index bcadf1f1..00000000 --- a/src/Scripts/plot_3DST.py +++ /dev/null @@ -1,68 +0,0 @@ -# plot_3DST.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 . - -# -*- coding: utf-8 -*- - -# a lancer depuis src -import sys -from matplotlib import pyplot as plt -from Model.Geometry.Reach import Reach -from numpy import mean - -def set_axes_equal(ax): - '''Make axes of 3D plot have equal scale so that spheres appear as spheres, - cubes as cubes, etc.. This is one possible solution to Matplotlib's - ax.set_aspect('equal') and ax.axis('equal') not working for 3D. - - Input - ax: a matplotlib axis, e.g., as output from plt.gca(). - ''' - - x_limits = ax.get_xlim3d() - y_limits = ax.get_ylim3d() - z_limits = ax.get_zlim3d() - - x_range = abs(x_limits[1] - x_limits[0]) - x_middle = mean(x_limits) - y_range = abs(y_limits[1] - y_limits[0]) - y_middle = mean(y_limits) - z_range = abs(z_limits[1] - z_limits[0]) - z_middle = mean(z_limits) - - # The plot bounding box is a sphere in the sense of the infinity - # norm, hence I call half the max range the plot radius. - plot_radius = 0.5*max([x_range, y_range, z_range]) - - ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) - ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) - ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) - -st_file = sys.argv[1] -my_reach = Reach(None) -my_reach.import_geometry(st_file) -my_reach.compute_guidelines() - -ax = plt.figure().add_subplot(projection="3d") -for x, y, z in zip(my_reach.get_x(), my_reach.get_y(), my_reach.get_z()): - ax.plot(x, y, z, color='r', lw=1.) -for x, y, z in zip(my_reach.get_guidelines_x(), my_reach.get_guidelines_y(), my_reach.get_guidelines_z()): - ax.plot(x, y, z, color='b', lw=1.) -ax.set_xlabel('X') -ax.set_ylabel('Y') -ax.set_zlabel('Z') -plt.tight_layout() -set_axes_equal(ax) -plt.show() diff --git a/src/pamhyr.py b/src/pamhyr.py index b4bd401f..0d595df4 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -34,6 +34,7 @@ from tools import ( from View.MainWindow import ApplicationWindow from Model.Study import Study +from Scripts.P3DST import Script3DST from Scripts.Hello import ScriptHello from init import license, setup_lang @@ -42,6 +43,7 @@ logger = logging.getLogger() scripts = { "hello": ScriptHello, + "3DST": Script3DST, } def usage(argv): From 2bb783b5d49cdd095cb0a9e5b533a61cebe8b291 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:09:55 +0200 Subject: [PATCH 005/248] ci: Add HTML documentation. --- .gitlab-ci.yml | 11 +++++++++++ doc/dev/build.sh | 11 +++++++++-- doc/users/build.sh | 11 +++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5f12c3e7..7f2aecdf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -109,9 +109,18 @@ build-developers-doc: script: - cd ./doc/dev/ - ./build.sh + # Create html documentation + - cd ../../src + - pdoc --html . + # Create archive + - tar cf html.tar ./html + - cd ../doc/dev + - mv ../../src/html.tar ./ artifacts: paths: - doc/dev/documentation.pdf + - doc/dev/documentation.html + - doc/dev/html.tar build-linux: stage: build @@ -167,8 +176,10 @@ build-linux: - cp ../tests_cases/Saar/Saar.pamhyr pamhyr/tests_cases/Saar/ # Copy documentations - mkdir -p pamhyr/doc/ + - mkdir -p pamhyr/doc/html/ - cp ../doc/users/documentation.pdf pamhyr/doc/Pamhyr2-users.pdf - cp ../doc/dev/documentation.pdf pamhyr/doc/Pamhyr2-dev.pdf + - cp ../doc/dev/documentation.html pamhyr/doc/html/dev.html # Create running script - echo "#!/bin/sh" > pamhyr/Pamhyr2 - echo "./pamhyr/pamhyr $@" >> pamhyr/Pamhyr2 diff --git a/doc/dev/build.sh b/doc/dev/build.sh index 476dd5ad..bf1a6790 100755 --- a/doc/dev/build.sh +++ b/doc/dev/build.sh @@ -1,10 +1,17 @@ #! /bin/sh -# Build the PDF - +echo " PDF documentation.org" emacs -Q -q -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-latex-export-to-pdf + +echo " HTML documentation.org" +emacs -Q -q -nw --batch \ + --eval "(require 'org)" \ + --eval "(require 'ox-latex)" \ + --eval "(load-file \"../tools/setup.el\")" \ + --file ./documentation.org \ + -f org-html-export-to-html diff --git a/doc/users/build.sh b/doc/users/build.sh index 476dd5ad..bf1a6790 100755 --- a/doc/users/build.sh +++ b/doc/users/build.sh @@ -1,10 +1,17 @@ #! /bin/sh -# Build the PDF - +echo " PDF documentation.org" emacs -Q -q -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-latex-export-to-pdf + +echo " HTML documentation.org" +emacs -Q -q -nw --batch \ + --eval "(require 'org)" \ + --eval "(require 'ox-latex)" \ + --eval "(load-file \"../tools/setup.el\")" \ + --file ./documentation.org \ + -f org-html-export-to-html From d461cb80019afd476e2cf7ea7e88258066be0158 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:26:44 +0200 Subject: [PATCH 006/248] doc: Fix htmlize into html build. --- doc/dev/build.sh | 3 +++ doc/users/build.sh | 3 +++ 2 files changed, 6 insertions(+) diff --git a/doc/dev/build.sh b/doc/dev/build.sh index bf1a6790..1afb7ca2 100755 --- a/doc/dev/build.sh +++ b/doc/dev/build.sh @@ -12,6 +12,9 @@ echo " HTML documentation.org" emacs -Q -q -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ + --eval "(require 'ox-html)" \ + --eval "(package-initialize)" \ + --eval "(require 'htmlize)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-html-export-to-html diff --git a/doc/users/build.sh b/doc/users/build.sh index bf1a6790..1afb7ca2 100755 --- a/doc/users/build.sh +++ b/doc/users/build.sh @@ -12,6 +12,9 @@ echo " HTML documentation.org" emacs -Q -q -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ + --eval "(require 'ox-html)" \ + --eval "(package-initialize)" \ + --eval "(require 'htmlize)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-html-export-to-html From 3842e689ac387dcbef3cb8f8eb68811d6b5ccea4 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:41:56 +0200 Subject: [PATCH 007/248] doc: Fix build package missing. --- doc/dev/build.sh | 6 ++---- doc/tools/setup.el | 12 ++++++++++++ doc/users/build.sh | 6 ++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/doc/dev/build.sh b/doc/dev/build.sh index 1afb7ca2..2e8deb56 100755 --- a/doc/dev/build.sh +++ b/doc/dev/build.sh @@ -1,7 +1,7 @@ #! /bin/sh echo " PDF documentation.org" -emacs -Q -q -nw --batch \ +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ @@ -9,12 +9,10 @@ emacs -Q -q -nw --batch \ -f org-latex-export-to-pdf echo " HTML documentation.org" -emacs -Q -q -nw --batch \ +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(require 'ox-html)" \ - --eval "(package-initialize)" \ - --eval "(require 'htmlize)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-html-export-to-html diff --git a/doc/tools/setup.el b/doc/tools/setup.el index 027e5700..e7d7b765 100644 --- a/doc/tools/setup.el +++ b/doc/tools/setup.el @@ -1,6 +1,18 @@ (require 'org) (require 'subr-x) +;; Needed packages + +(require 'package) +(add-to-list 'package-archives + '("melpa" . "https://melpa.org/packages/")) + +(package-initialize) + +;;; Install HTMLIZE +(package-install 'htmlize) +(require 'htmlize) + ;; LaTeX config (add-to-list diff --git a/doc/users/build.sh b/doc/users/build.sh index 1afb7ca2..2e8deb56 100755 --- a/doc/users/build.sh +++ b/doc/users/build.sh @@ -1,7 +1,7 @@ #! /bin/sh echo " PDF documentation.org" -emacs -Q -q -nw --batch \ +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ @@ -9,12 +9,10 @@ emacs -Q -q -nw --batch \ -f org-latex-export-to-pdf echo " HTML documentation.org" -emacs -Q -q -nw --batch \ +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(require 'ox-html)" \ - --eval "(package-initialize)" \ - --eval "(require 'htmlize)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-html-export-to-html From 9c7f1f7ba939649cd27eac4ed943d7fc0aae5925 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:51:00 +0200 Subject: [PATCH 008/248] doc: Try with system package. --- doc/tools/setup.el | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/doc/tools/setup.el b/doc/tools/setup.el index e7d7b765..027e5700 100644 --- a/doc/tools/setup.el +++ b/doc/tools/setup.el @@ -1,18 +1,6 @@ (require 'org) (require 'subr-x) -;; Needed packages - -(require 'package) -(add-to-list 'package-archives - '("melpa" . "https://melpa.org/packages/")) - -(package-initialize) - -;;; Install HTMLIZE -(package-install 'htmlize) -(require 'htmlize) - ;; LaTeX config (add-to-list From fa32e42933337635c2d8c9717c5fe346d4cb95b1 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 15:10:30 +0200 Subject: [PATCH 009/248] refactoring: Add PamhyrWindow and PamhyrDialog (WIP). --- src/View/About/Window.py | 19 ++- src/View/Configure/Solver/Window.py | 26 +++- src/View/Configure/Window.py | 117 ++++++++------- src/View/DummyWindow.py | 15 +- src/View/MainWindow.py | 130 ++++++++-------- src/View/Network/Table.py | 1 - src/View/Network/Window.py | 45 +++--- src/View/Study/Window.py | 48 +++--- src/View/{ => Tools}/ASubWindow.py | 10 +- src/View/{ => Tools}/ListedSubWindow.py | 2 +- src/View/Tools/PamhyrWindow.py | 189 ++++++++++++++++++++++++ 11 files changed, 408 insertions(+), 194 deletions(-) rename src/View/{ => Tools}/ASubWindow.py (97%) rename src/View/{ => Tools}/ListedSubWindow.py (98%) create mode 100644 src/View/Tools/PamhyrWindow.py diff --git a/src/View/About/Window.py b/src/View/About/Window.py index 7c269aaf..218869b5 100644 --- a/src/View/About/Window.py +++ b/src/View/About/Window.py @@ -19,14 +19,17 @@ import os import logging -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtCore import QCoreApplication _translate = QCoreApplication.translate logger = logging.getLogger() -class AboutWindow(ASubWindow): +class AboutWindow(PamhyrDialog): + _pamhyr_ui = "about" + _pamhyr_name = "About" + def _path_file(self, filename): return os.path.abspath( os.path.join( @@ -35,10 +38,14 @@ class AboutWindow(ASubWindow): ) ) - - def __init__(self, title="About", parent=None): - super(AboutWindow, self).__init__(name=title, ui="about", parent=parent) - self.ui.setWindowTitle(title) + def __init__(self, study = None, config = None, parent=None): + super(AboutWindow, self).__init__( + title = _translate("About", "About"), + study = study, + config = config, + options = [], + parent = parent + ) # Version with open(self._path_file("VERSION"), "r") as f: diff --git a/src/View/Configure/Solver/Window.py b/src/View/Configure/Solver/Window.py index c295481f..369ff40a 100644 --- a/src/View/Configure/Solver/Window.py +++ b/src/View/Configure/Solver/Window.py @@ -16,7 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from Solver.Solvers import solver_type_list from Solver.GenericSolver import GenericSolver @@ -24,12 +24,22 @@ from PyQt5.QtWidgets import ( QPushButton, ) -class ConfigureSolverWindow(ASubWindow): - def __init__(self, data=None, title="Configuration : Add Solver", parent=None): +class ConfigureSolverWindow(PamhyrDialog): + _pamhyr_ui = "ConfigureAddSolverDialog" + _pamhyr_name = "Add/Edit Solver" + + def __init__(self, data=None, config=None, parent=None): + if data is not None: + name = f"Edit Solver - {data.name}" + else: + name = "Add a new Solver" + super(ConfigureSolverWindow, self).__init__( - name=title, ui="ConfigureAddSolverDialog", parent=parent + title = name, + config = config, + options = [], + parent=parent ) - self.ui.setWindowTitle(title) # Combo box item for solver in solver_type_list: @@ -37,10 +47,10 @@ class ConfigureSolverWindow(ASubWindow): # Data to return self.data = data - if not self.data is None: + if self.data is not None: self.copy_data() - self.connect() + self.setup_connection() def copy_data(self): self.set_combobox_text("comboBox_solver", self.data.type) @@ -53,7 +63,7 @@ class ConfigureSolverWindow(ASubWindow): self.set_line_edit_text("lineEdit_output", self.data._path_output) self.set_line_edit_text("lineEdit_output_cmd", self.data._cmd_output) - def connect(self): + def setup_connection(self): # File button buttons = { "pushButton_input": (lambda: self.file_dialog( diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index 23a371ac..83db81f6 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -21,8 +21,7 @@ import logging from copy import deepcopy from config import Config -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from View.Stricklers.Table import TableModel from View.Stricklers.translate import * @@ -95,50 +94,42 @@ class SolverTableModel(QAbstractTableModel): self.layoutChanged.emit() -class ConfigureWindow(ASubWindow, ListedSubWindow): - def __init__(self, conf=None, title="Configure", parent=None): +class ConfigureWindow(PamhyrDialog): + _pamhyr_ui = "ConfigureDialog" + _pamhyr_name = "Configure" + + def __init__(self, config=None, parent=None): + if config is None: + config = Config() + super(ConfigureWindow, self).__init__( - name=title, ui="ConfigureDialog", parent=parent + title = self._pamhyr_name, + config = config, + options = [], + parent=parent ) - self.ui.setWindowTitle(title) - if conf is None: - self.conf = Config() - else: - self.conf = conf + self.setup_custom_sc() + self.setup_solver() + self.setup_stricklers() + self.setup_data() + self.setup_connection() - self.setup_sc() - - # Solver + def setup_solver(self): table = self.find(QTableView, "tableView_solver") self.solver_table_model = SolverTableModel( headers = ["name", "type", "description"], - rows = conf.solvers + rows = self._config.solvers ) table.setModel(self.solver_table_model) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) + table.resizeColumnsToContents() - # Meshing_Tool - self.set_line_edit_text("lineEdit_meshing_tool", self.conf.meshing_tool) - - # Const - self.set_line_edit_text("lineEdit_segment", str(self.conf.segment)) - self.set_line_edit_text("lineEdit_max_listing", str(self.conf.max_listing)) - - # Backup - self.set_check_box("checkBox_backup", self.conf.backup_enable) - self.set_line_edit_text("lineEdit_backup_path", self.conf.backup_path) - self.set_time_edit("timeEdit_backup_frequence", self.conf.backup_frequence) - self.set_spin_box("spinBox_backup_max", self.conf.backup_max) - - self.find(QTableView, "tableView_solver").resizeColumnsToContents() - self.connect() - - # Stricklers + def setup_stricklers(self): table = self.find(QTableView, f"tableView_stricklers") - self._stricklers = deepcopy(self.conf.stricklers) + self._stricklers = deepcopy(self._config.stricklers) self._stricklers_table = TableModel( data = self._stricklers, undo = self._undo_stack, @@ -148,26 +139,35 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) + def setup_data(self): + # Meshing_Tool + self.set_line_edit_text("lineEdit_meshing_tool", self._config.meshing_tool) + + # Const + self.set_line_edit_text("lineEdit_segment", str(self._config.segment)) + self.set_line_edit_text("lineEdit_max_listing", str(self._config.max_listing)) + + # Backup + self.set_check_box("checkBox_backup", self._config.backup_enable) + self.set_line_edit_text("lineEdit_backup_path", self._config.backup_path) + self.set_time_edit("timeEdit_backup_frequence", self._config.backup_frequence) + self.set_spin_box("spinBox_backup_max", self._config.backup_max) + # Editor - self.set_line_edit_text("lineEdit_editor_cmd", str(self.conf.editor)) + self.set_line_edit_text("lineEdit_editor_cmd", str(self._config.editor)) # Language languages = Config.languages() for lang in languages: self.combobox_add_item("comboBox_language", lang) - if self.conf.lang == languages[lang]: + if self._config.lang == languages[lang]: self.set_combobox_text("comboBox_language", lang) - def setup_sc(self): - self._undo_stack = QUndoStack() + def setup_custom_sc(self): + self._debug_sc = QShortcut(QKeySequence("Ctrl+G"), self) + self._debug_sc.activated.connect(self.set_debug) - self.debug_sc = QShortcut(QKeySequence("Ctrl+G"), self) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def connect(self): + def setup_connection(self): buttons = { # Solvers "pushButton_solver_add": self.add_solver, @@ -195,33 +195,31 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): for button in buttons: self.find(QPushButton, button).clicked.connect(buttons[button]) - self.debug_sc.activated.connect(self.set_debug) - def accept(self): # Solvers - self.conf.solvers = self.solver_table_model.rows.copy() + self._config.solvers = self.solver_table_model.rows.copy() # Meshing_Tool - self.conf.meshing_tool = self.get_line_edit_text("lineEdit_meshing_tool") + self._config.meshing_tool = self.get_line_edit_text("lineEdit_meshing_tool") # Const - self.conf.segment = self.get_line_edit_text("lineEdit_segment") - self.conf.max_listing = self.get_line_edit_text("lineEdit_max_listing") + self._config.segment = self.get_line_edit_text("lineEdit_segment") + self._config.max_listing = self.get_line_edit_text("lineEdit_max_listing") # Backup - self.conf.backup_enable = self.get_check_box("checkBox_backup") - self.conf.backup_path = self.get_line_edit_text("lineEdit_backup_path") - self.conf.backup_frequence = self.get_time_edit("timeEdit_backup_frequence") - self.conf.backup_max = self.get_spin_box("spinBox_backup_max") + self._config.backup_enable = self.get_check_box("checkBox_backup") + self._config.backup_path = self.get_line_edit_text("lineEdit_backup_path") + self._config.backup_frequence = self.get_time_edit("timeEdit_backup_frequence") + self._config.backup_max = self.get_spin_box("spinBox_backup_max") # Stricklers - self.conf.stricklers = deepcopy(self._stricklers) + self._config.stricklers = deepcopy(self._stricklers) # Editor - self.conf.editor = self.get_line_edit_text("lineEdit_editor_cmd") + self._config.editor = self.get_line_edit_text("lineEdit_editor_cmd") # Language - self.conf.lang = Config.languages()[self.get_combobox_text("comboBox_language")] + self._config.lang = Config.languages()[self.get_combobox_text("comboBox_language")] self.end() @@ -230,13 +228,13 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): self.end() def end(self): - self.conf.save() + self._config.save() self.close() # Debug def set_debug(self): - self.conf.debug = not self.conf.debug - logger.info(f"Debug mode set : {self.conf.debug}") + self._config.debug = not self._config.debug + logger.info(f"Debug mode set : {self._config.debug}") self.parent.setup_debug_mode() # Solvers @@ -246,6 +244,7 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): for index in indexes: self.edit_solver = ConfigureSolverWindow( data=self.solver_table_model.rows[index.row()], + config=self._config, parent=self ) if self.edit_solver.exec_(): diff --git a/src/View/DummyWindow.py b/src/View/DummyWindow.py index 05117bca..768d6fbc 100644 --- a/src/View/DummyWindow.py +++ b/src/View/DummyWindow.py @@ -16,9 +16,14 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrWindow -class DummyWindow(ASubWindow): - def __init__(self, title="Dummy", parent=None): - super(DummyWindow, self).__init__(name=title, ui="dummy", parent=parent) - self.ui.setWindowTitle(title) +class DummyWindow(PamhyrWindow): + __ui = "Dummy" + __name = "Pamhyr Dummy Window" + + def __init__(self, parent=None): + super(DummyWindow, self).__init__( + title = self.__name, + parent = parent, + ) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 47e14850..cf253dd5 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -36,27 +36,27 @@ from PyQt5.QtWidgets import ( ) from PyQt5.uic import loadUi -from View.ASubWindow import WindowToolKit -from View.ListedSubWindow import ListedSubWindow +from View.Tools.ASubWindow import WindowToolKit +from View.Tools.ListedSubWindow import ListedSubWindow from View.DummyWindow import DummyWindow from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow -from View.Geometry.Window import GeometryWindow -from View.BoundaryCondition.Window import BoundaryConditionWindow -from View.LateralContribution.Window import LateralContributionWindow -from View.InitialConditions.Window import InitialConditionsWindow -from View.Stricklers.Window import StricklersWindow -from View.Frictions.Window import FrictionsWindow -from View.SedimentLayers.Window import SedimentLayersWindow -from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow -from View.SolverParameters.Window import SolverParametersWindow -from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -from View.CheckList.Window import CheckListWindow -from View.Results.Window import ResultsWindow -from View.Debug.Window import ReplWindow +# from View.Geometry.Window import GeometryWindow +# from View.BoundaryCondition.Window import BoundaryConditionWindow +# from View.LateralContribution.Window import LateralContributionWindow +# from View.InitialConditions.Window import InitialConditionsWindow +# from View.Stricklers.Window import StricklersWindow +# from View.Frictions.Window import FrictionsWindow +# from View.SedimentLayers.Window import SedimentLayersWindow +# from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow +# from View.SolverParameters.Window import SolverParametersWindow +# from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow +# from View.CheckList.Window import CheckListWindow +# from View.Results.Window import ResultsWindow +# from View.Debug.Window import ReplWindow from Model.Study import Study @@ -108,7 +108,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.conf = conf # Model - self.model = None + self._study = None # Results self._last_results = None @@ -133,8 +133,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def set_title(self): title = "(dbg) " if self.conf.debug else "" - if self.model is not None: - title += f"Pamhyr2 - {self.model.name}" + if self._study is not None: + title += f"Pamhyr2 - {self._study.name}" self.setWindowTitle(title) else: title += "Pamhyr2" @@ -216,7 +216,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): super(ApplicationWindow, self).changeEvent(event) def close(self): - if self.model is not None and not self.model.is_saved: + if self._study is not None and not self._study.is_saved: self._close_question = True if self.dialog_close(): # PAMHYR is close correctly (no crash) @@ -233,7 +233,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def closeEvent(self, event): if not self._close_question: - if self.model is not None and not self.model.is_saved: + if self._study is not None and not self._study.is_saved: if self.dialog_close(cancel = False): # PAMHYR is close correctly (no crash) self.conf.set_close_correctly() @@ -291,16 +291,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ######### def get_model(self): - return self.model + return self._study def set_model(self, model): - self.model = model + self._study = model self.update_enable_action() - self.conf.set_last_study(self.model.filename) + self.conf.set_last_study(self._study.filename) self.set_title() def close_model(self): - self.model = None + self._study = None self.update_enable_action() self.conf.set_close_correctly() self.set_title() @@ -314,7 +314,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - no_model = self.model is None + no_model = self._study is None for action in no_model_action: self.enable_actions(action, no_model) @@ -342,7 +342,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Nothing """ self.set_model(Study.open(filename)) - logger.info(f"Open Study - {self.model.name}") + logger.info(f"Open Study - {self._study.name}") self.set_title() def save_study(self): @@ -354,22 +354,22 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model.filename is None or self.model.filename == "": + if self._study.filename is None or self._study.filename == "": file_name, _ = QFileDialog.getSaveFileName( self, "Save File", "", "Pamhyr(*.pamhyr)" ) if file_name.rsplit(".", 1)[-1] == "pamhyr": - self.model.filename = file_name + self._study.filename = file_name else: - self.model.filename = file_name + ".pamhyr" + self._study.filename = file_name + ".pamhyr" - if self.model.is_saved: + if self._study.is_saved: return logger.info("Save...") - self.model.save() + self._study.save() def save_as_study(self): """Save current study as new file @@ -386,11 +386,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) if file_name[-4:] == ".pamhyr": - self.model.filename = file_name + self._study.filename = file_name else: - self.model.filename = file_name + ".pamhyr" + self._study.filename = file_name + ".pamhyr" - self.model.save() + self._study.save() ################## # MSG AND DIALOG # @@ -456,7 +456,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - self.config = ConfigureWindow(conf=self.conf, parent=self) + self.config = ConfigureWindow(config=self.conf, parent=self) self.config.show() def open_about(self): @@ -476,7 +476,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is None: + if self._study is None: dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.FileMode.ExistingFile) dialog.setDefaultSuffix(".pamhyr") @@ -494,7 +494,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is None: + if self._study is None: self.new_study = NewStudyWindow(parent=self) self.new_study.show() @@ -504,8 +504,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if not self.model is None: - self.new_study = NewStudyWindow(study=self.model, parent=self) + if not self._study is None: + self.new_study = NewStudyWindow(study=self._study, parent=self) self.new_study.show() def open_network(self): @@ -514,9 +514,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is not None: + if self._study is not None: if not self.sub_win_exists("River network"): - self.network = NetworkWindow(model=self.model, parent=self) + self.network = NetworkWindow(study=self._study, parent=self) self.network.show() else: self.network.activateWindow() @@ -527,14 +527,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if (self.model is not None and self.model.river.has_current_reach()): + if (self._study is not None and self._study.river.has_current_reach()): geometry = self.sub_win_filter_first( "Geometry", - contain = [self.model.river.current_reach().name] + contain = [self._study.river.current_reach().name] ) if geometry is None: - geometry = GeometryWindow(model=self.model, parent=self) + geometry = GeometryWindow(model=self._study, parent=self) geometry.show() else: geometry.activateWindow() @@ -548,7 +548,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) if bound is None: - bound = BoundaryConditionWindow(study = self.model, parent = self) + bound = BoundaryConditionWindow(study = self._study, parent = self) bound.show() else: bound.activateWindow() @@ -560,7 +560,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) if lateral is None: - lateral = LateralContributionWindow(study = self.model, parent = self) + lateral = LateralContributionWindow(study = self._study, parent = self) lateral.show() else: lateral.activateWindow() @@ -573,7 +573,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if strick is None: strick = StricklersWindow( - study = self.model, + study = self._study, config = self.conf, parent = self ) @@ -582,17 +582,17 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): strick.activateWindow() def open_frictions(self): - if (self.model is not None and - self.model.river.has_current_reach()): + if (self._study is not None and + self._study.river.has_current_reach()): frictions = self.sub_win_filter_first( "Frictions", - contain = [self.model.river.current_reach().name] + contain = [self._study.river.current_reach().name] ) if frictions is None: frictions = FrictionsWindow( - study = self.model, + study = self._study, parent = self ) frictions.show() @@ -602,15 +602,15 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.msg_select_reach() def open_initial_conditions(self): - if self.model.river.has_current_reach(): + if self._study.river.has_current_reach(): initial = self.sub_win_filter_first( "Initial condition", - contain = [self.model.river.current_reach().name] + contain = [self._study.river.current_reach().name] ) if initial is None: initial = InitialConditionsWindow( - study = self.model, + study = self._study, parent = self ) initial.show() @@ -627,7 +627,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if params is None: params = SolverParametersWindow( - study = self.model, + study = self._study, parent = self ) params.show() @@ -636,31 +636,31 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_sediment_layers(self): sl = SedimentLayersWindow( - study = self.model, + study = self._study, parent = self ) sl.show() def open_reach_sediment_layers(self): sl = ReachSedimentLayersWindow( - study = self.model, + study = self._study, parent = self ) sl.show() def run_solver(self): - if self.model is None: + if self._study is None: return run = SelectSolverWindow( - study = self.model, + study = self._study, config = self.conf, parent = self ) if run.exec(): solver = run.solver check = CheckListWindow( - study = self.model, + study = self._study, config = self.conf, solver = solver, parent = self @@ -669,7 +669,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def solver_log(self, solver): sol = SolverLogWindow( - study = self.model, + study = self._study, config = self.conf, solver = solver, parent = self @@ -693,7 +693,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if res is None: res = ResultsWindow( - study = self.model, + study = self._study, solver = solver, results = results, parent = self @@ -708,18 +708,18 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_debug(self): repl = ReplWindow( - study = self.model, + study = self._study, config = self.conf, parent = self ) repl.show() def open_sqlite(self): - if self.model is None: + if self._study is None: logger.debug("No study open for sql debuging...") return - file = self.model.filename + file = self._study.filename _ = subprocess.Popen( f"sqlitebrowser {file}", shell=True diff --git a/src/View/Network/Table.py b/src/View/Network/Table.py index 733a910d..da74d813 100644 --- a/src/View/Network/Table.py +++ b/src/View/Network/Table.py @@ -22,7 +22,6 @@ import traceback from Model.Network.Node import Node from Model.Network.Edge import Edge from Model.Network.Graph import Graph -from View.ASubWindow import ASubWindow from View.Network.GraphWidget import GraphWidget from View.Network.UndoCommand import * from View.Tools.PamhyrTable import PamhyrTableModel diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index 95e575ea..a54c0527 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +import logging + from PyQt5.QtCore import QCoreApplication from PyQt5.QtGui import ( @@ -36,7 +38,7 @@ from PyQt5.QtWidgets import ( from Model.River import RiverNode, RiverReach, River -from View.ASubWindow import ASubMainWindow +from View.Tools.PamhyrWindow import PamhyrWindow from View.Network.GraphWidget import GraphWidget from View.Network.UndoCommand import * from View.Network.translate import ( @@ -47,36 +49,30 @@ from View.Network.Table import ( ComboBoxDelegate, NodeTableModel, EdgeTableModel, ) +logger = logging.getLogger() + _translate = QCoreApplication.translate -class NetworkWindow(ASubMainWindow): - def __init__(self, model=None, title="River network", parent=None): - self._title = title - self._model = model - self._graph = self._model.river - - self.setup_title() +class NetworkWindow(PamhyrWindow): + _pamhyr_ui = "Network" + _pamhyr_name = "River network" + def __init__(self, study=None, config=None, parent=None): super(NetworkWindow, self).__init__( - name=title, ui="Network", parent=parent + title = self._pamhyr_name + " - " + study.name, + study = study, + config = config, + options = ['undo'], + parent=parent, ) - self.ui.setWindowTitle(self._title) - self.setup_sc() + self._graph = study.river + self.setup_graph() self.setup_table() self.setup_connections() - def setup_title(self): - self._title = self._title + " - " + self._model.name - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - def setup_table(self): retranslate() @@ -143,9 +139,6 @@ class NetworkWindow(ASubMainWindow): self.clicked_del ) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - def clicked_add(self): if self.get_action_checkable("action_toolBar_add"): self.set_action_checkable("action_toolBar_del", False) @@ -166,13 +159,15 @@ class NetworkWindow(ASubMainWindow): if key == Qt.Key_Escape: self._graph_widget.reset_selection - def undo(self): + # Redefine undo/redo method + + def _undo(self): self._undo_stack.undo() self._reachs_model.update() self._nodes_model.update() self._graph_widget.display_update() - def redo(self): + def _redo(self): self._undo_stack.redo() self._reachs_model.update() self._nodes_model.update() diff --git a/src/View/Study/Window.py b/src/View/Study/Window.py index 7218f9d3..871d0545 100644 --- a/src/View/Study/Window.py +++ b/src/View/Study/Window.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from Model.Study import Study -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtCore import QCoreApplication @@ -27,27 +27,37 @@ from PyQt5.QtWidgets import ( _translate = QCoreApplication.translate -class NewStudyWindow(ASubWindow): - def __init__(self, study=None, title="New Study", parent=None): - super(NewStudyWindow, self).__init__(name=title, ui="NewStudy", parent=parent) - self.ui.setWindowTitle(title) +class NewStudyWindow(PamhyrDialog): + _pamhyr_ui = "NewStudy" + _pamhyr_name = "Edit/New Study" - self.parent = parent - self.study = study + def __init__(self, study=None, config = None, parent=None): + if study is not None: + name = f"Edit study - {study.name}" + else: + name = "New study" - if not self.study is None: - self.set_line_edit_text("lineEdit_name", self.study.name) - self.set_text_edit_text("textEdit_description", self.study.description) - self.set_datetime_edit("dateTimeEdit_date", self.study.date) + super(NewStudyWindow, self).__init__( + title = name, + study = study, + config = config, + options = [], + parent = parent + ) + + if not self._study is None: + self.set_line_edit_text("lineEdit_name", self._study.name) + self.set_text_edit_text("textEdit_description", self._study.description) + self.set_datetime_edit("dateTimeEdit_date", self._study.date) self.find(QLabel, "label_creation_date_data").setText( - self.study.creation_date.isoformat(sep=" ") + self._study.creation_date.isoformat(sep=" ") ) self.find(QLabel, "label_last_modification_data").setText( - self.study.last_save_date.isoformat(sep=" ") + self._study.last_save_date.isoformat(sep=" ") ) - if self.study.time_system == "date": + if self._study.time_system == "date": self.set_radio_button("radioButton_date", True) self.find(QLabel, "label_date").setEnabled(True) self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True) @@ -76,18 +86,18 @@ class NewStudyWindow(ASubWindow): name = self.get_line_edit_text("lineEdit_name") description = self.get_text_edit_text("textEdit_description") - if self.study is None: + if self._study is None: study = Study.new(name, description) if self.get_radio_button("radioButton_date"): date = self.get_datetime_edit("dateTimeEdit_date") study.use_date(date) self.parent.set_model(study) else: - self.study.name = name - self.study.description = description + self._study.name = name + self._study.description = description if self.get_radio_button("radioButton_date"): date = self.get_datetime_edit("dateTimeEdit_date") - self.study.use_date(date) + self._study.use_date(date) else: - self.study.use_time() + self._study.use_time() self.done(True) diff --git a/src/View/ASubWindow.py b/src/View/Tools/ASubWindow.py similarity index 97% rename from src/View/ASubWindow.py rename to src/View/Tools/ASubWindow.py index 4e1c1327..9ac63c2d 100644 --- a/src/View/ASubWindow.py +++ b/src/View/Tools/ASubWindow.py @@ -463,11 +463,11 @@ class ASubWindowFeatures(object): # Top level interface class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): - def __init__(self, name="", ui="dummy", parent=None): + def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubMainWindow, self).__init__(parent=parent) if ui is not None: self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", f"{ui}.ui"), + os.path.join(os.path.dirname(__file__), "..", "ui", f"{ui}.ui"), self ) @@ -496,10 +496,10 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): return self.ui.findChild(qtype, name) class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): - def __init__(self, name="", ui="dummy", parent=None): + def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubWindow, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", f"{ui}.ui"), + os.path.join(os.path.dirname(__file__), "..", "ui", f"{ui}.ui"), self ) self.name = name @@ -530,7 +530,7 @@ class AWidget(QWidget, ASubWindowFeatures): def __init__(self, ui="", parent=None): super(AWidget, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", "Widgets", f"{ui}.ui"), + os.path.join(os.path.dirname(__file__), "..", "ui", "Widgets", f"{ui}.ui"), self ) self.parent = parent diff --git a/src/View/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py similarity index 98% rename from src/View/ListedSubWindow.py rename to src/View/Tools/ListedSubWindow.py index b4dfe416..f671cc60 100644 --- a/src/View/ListedSubWindow.py +++ b/src/View/Tools/ListedSubWindow.py @@ -22,7 +22,7 @@ from functools import reduce logger = logging.getLogger() class ListedSubWindow(object): - def __init__(self): + def __init__(self, **kwargs): super(ListedSubWindow, self).__init__() self.sub_win_cnt = 0 self.sub_win_list = [] diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py new file mode 100644 index 00000000..d6ae6752 --- /dev/null +++ b/src/View/Tools/PamhyrWindow.py @@ -0,0 +1,189 @@ +# PamhyrWindow.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 . + +# -*- coding: utf-8 -*- + +import logging + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtWidgets import ( + QUndoStack, QShortcut, +) + +from View.Tools.ASubWindow import ASubMainWindow, ASubWindow +from View.Tools.ListedSubWindow import ListedSubWindow + +logger = logging.getLogger() + +class PamhyrWindowTools(object): + def __init__(self, options = ["undo", "copy"], parent = None, **kwargs): + super(PamhyrWindowTools, self).__init__() + + self._undo_stack = None + + if "undo" in options: + self._init_undo() + + if "copy" in options: + self._init_copy() + + # Undo/Redo + + def _init_undo(self): + self._undo_stack = QUndoStack() + + self._undo_sc = QShortcut(QKeySequence.Undo, self) + self._redo_sc = QShortcut(QKeySequence.Redo, self) + + self._undo_sc.activated.connect(self._undo) + self._redo_sc.activated.connect(self._redo) + + def _undo(self): + if self._undo_stack is not None: + self._undo_stack.undo() + self._update() + + def _redo(self): + if self._undo_stack is not None: + self._undo_stack.redo() + self._update() + + # Copy/Paste + + def _init_copy(self): + self._copy_sc = QShortcut(QKeySequence.Copy, self) + self._paste_sc = QShortcut(QKeySequence.Paste, self) + + self._copy_sc.activated.connect(self._copy) + self._paste_sc.activated.connect(self._paste) + + def _copy(self): + if self._copy_stack is not None: + self._copy_stack.copy() + self._update() + + def _paste(self): + if self._copy_stack is not None: + self._copy_stack.redo() + self._update() + + # Display + + def _set_title(self): + """Apply `self._title` at current window title displayed + + Returns: + Nothing + """ + self.ui.setWindowTitle(self._title) + + def _update(self): + """Update window display component + + Returns: + Nothing + """ + self._set_title() + + # Hash methods + + @classmethod + def _hash(cls, data): + """Compute window hash + + Args: + data: window data parameters + + Returns: + The hash + """ + hash_str = cls._pamhyr_name + hash_str += cls._pamhyr_ui + + for el in data: + hash_str += repr(el) + + h = hash(hash_str) + logger.debug(f"Compute hash = {h} for window {cls._pamhyr_name}") + + return h + + def hash(self): + """Compute window hash + + Returns: + The hash + """ + data = [ + self._study, + self._config, + ] + + return self._hash() + + +class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): + _pamhyr_ui = "dummy" + _pamhyr_name = "PamhyrWindow" + + def __init__(self, + title = "Pamhyr2", + study = None, config = None, + options = ["undo", "copy"], + parent = None): + self._title = title + self._study = study + self._config = config + self._parent = parent + + logger.info(self._pamhyr_name) + + super(PamhyrWindow, self).__init__( + name = self._pamhyr_name, + ui = self._pamhyr_ui, + parent = parent, + ) + + self._set_title() + + +class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools): + _pamhyr_ui = "dummy" + _pamhyr_name = "PamhyrWindow" + + def __init__(self, + title = "Pamhyr2", + study = None, config = None, + options = ["undo", "copy"], + parent = None): + self._title = title + self._study = study + self._config = config + self._parent = parent + + logger.info(self._pamhyr_name) + logger.info(self._pamhyr_ui) + + super(PamhyrDialog, self).__init__( + name = self._pamhyr_name, + ui = self._pamhyr_ui, + parent = parent, + ) + + self._set_title() From cc9f853a34498049af3092d1688b297922a28fe1 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 15:31:41 +0200 Subject: [PATCH 010/248] refactoring: Add PamhyrWidget and continue refacto using PamhyrWindow. --- src/View/BoundaryCondition/Edit/Table.py | 2 - src/View/BoundaryCondition/Edit/Window.py | 75 +++++++++-------------- src/View/BoundaryCondition/Window.py | 40 +++++------- src/View/Geometry/Profile/Window.py | 3 +- src/View/Geometry/Window.py | 5 +- src/View/MainWindow.py | 4 +- src/View/Plot/PamhyrToolbar.py | 2 - src/View/Tools/PamhyrWidget.py | 13 +++- 8 files changed, 64 insertions(+), 80 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 35360400..e89e6d0d 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -23,8 +23,6 @@ from datetime import date, time, datetime, timedelta from tools import trace, timer -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow from View.Tools.PamhyrTable import PamhyrTableModel from PyQt5.QtCore import ( diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 6e0d6131..249a34cb 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -20,8 +20,8 @@ import logging from tools import timer, trace -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.PamhyrWidget import PamhyrWidget from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate from PyQt5.QtGui import ( @@ -54,16 +54,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class WD50Sigma(AWidget): +class WD50Sigma(PamhyrWidget): + _pamhyr_ui = "d50sigma" + d50Changed = pyqtSignal(float) sigmaChanged = pyqtSignal(float) def __init__(self, parent=None): super(WD50Sigma, self).__init__( - ui="d50sigma", parent=parent ) - self.parent = parent self.spinBox_d50 = self.find(QDoubleSpinBox, "doubleSpinBox_d50") self.spinBox_sigma = self.find(QDoubleSpinBox, "doubleSpinBox_sigma") @@ -95,45 +95,37 @@ class WD50Sigma(AWidget): def valueChangedSigma(self, value): self.sigmaChanged.emit(value) -class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit boundary condition", - data=None, study=None, parent=None): +class EditBoundaryConditionWindow(PamhyrWindow): + _pamhyr_ui = "EditBoundaryConditions" + _pamhyr_name = "Edit Boundary Conditions" + + def __init__(self, data=None, study=None, config=None, parent=None): self._data = data - self._study = study - self._title = title - self.compute_title() - - super(EditBoundaryConditionWindow, self).__init__( - name=self._title, ui="EditBoundaryConditions", parent=parent - ) - - self.ui.setWindowTitle(self._title) - - self.setup_sc() - self.setup_table() - self.setup_plot() - self.setup_data() - self.setup_connections() - - def compute_title(self): + name = self._pamhyr_name if self._data is not None: node_name = (self._data.node.name if self._data.node is not None else _translate("BoundaryCondition", "Not associate")) - self._title = ( - _translate("Edit boundary condition", self._title) + - f" - {self._study.name} " + + name = ( + _translate("Edit boundary condition", self._pamhyr_name) + + f" - {study.name} " + f" - {self._data.name} ({self._data.id}) " + f"({long_types[self._data.bctype]} - {node_name})" ) - def setup_sc(self): - self._undo_stack = QUndoStack() + super(EditBoundaryConditionWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent + ) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self.ui.setWindowTitle(self._title) + + self.setup_table() + self.setup_plot() + self.setup_data() + self.setup_connections() def setup_data(self): self._is_solid = self._data.bctype == "SL" @@ -193,17 +185,11 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self.update) if self._is_solid: @@ -252,7 +238,6 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): ) ) - def add(self): rows = self.index_selected_rows() if len(self._data) == 0 or len(rows) == 0: @@ -285,7 +270,7 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.plot.update() - def copy(self): + def _copy(self): rows = self.index_selected_rows() table = [] @@ -297,7 +282,7 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -311,12 +296,12 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self._table.paste(row, header, data) self.plot.update() - def undo(self): + def _undo(self): self._table.undo() self.plot.update() self.widget_update() - def redo(self): + def _redo(self): self._table.redo() self.plot.update() self.widget_update() diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index 7f8c13e2..5f08ecf8 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -63,32 +62,28 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Boundary conditions", study=None, parent=None): - self._title = title + " - " + study.name +class BoundaryConditionWindow(PamhyrWindow): + _pamhyr_ui = "BoundaryConditions" + _pamhyr_name = "Boundary conditions" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(BoundaryConditionWindow, self).__init__( - name=title, ui="BoundaryConditions", parent=parent + title = name, + study = study, + config = config, + parent=parent ) - self._study = study self._bcs = self._study.river.boundary_condition - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() self._table = {} @@ -141,11 +136,6 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_edit").triggered.connect(self.edit) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def current_tab(self): return self.find(QTabWidget, "tabWidget")\ .currentWidget()\ @@ -202,17 +192,17 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): row = self.index_selected_row() self._table[tab].move_down(row) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 54d1e011..eca6e7af 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -37,7 +37,8 @@ from PyQt5.QtWidgets import ( from Model.Geometry.Reach import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ -from View.ASubWindow import ASubMainWindow +from View.Tools.ASubWindow import ASubMainWindow + from View.Geometry.Profile.mainwindow_ui_profile import Ui_MainWindow from View.Geometry.Profile.Plot import Plot from View.Geometry.Profile.Table import * diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 1c8cb5da..a66ed781 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -41,8 +41,9 @@ from View.Geometry.PlotXY import PlotXY from View.Geometry.PlotKPZ import PlotKPZ from View.Geometry.PlotAC import PlotAC -from View.ASubWindow import ASubMainWindow, WindowToolKit -from View.ListedSubWindow import ListedSubWindow +from View.Tools.ASubWindow import ASubMainWindow, WindowToolKit +from View.Tools.ListedSubWindow import ListedSubWindow + from View.Geometry.mainwindow_ui_reach import Ui_MainWindow from View.Geometry.Table import * from View.Geometry.Profile.Window import ProfileWindow diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index cf253dd5..f701a474 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -44,8 +44,8 @@ from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow -# from View.Geometry.Window import GeometryWindow -# from View.BoundaryCondition.Window import BoundaryConditionWindow +from View.Geometry.Window import GeometryWindow +from View.BoundaryCondition.Window import BoundaryConditionWindow # from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow diff --git a/src/View/Plot/PamhyrToolbar.py b/src/View/Plot/PamhyrToolbar.py index 6a729f04..c88dfacf 100644 --- a/src/View/Plot/PamhyrToolbar.py +++ b/src/View/Plot/PamhyrToolbar.py @@ -81,8 +81,6 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): os.path.abspath(f"{file_path}/../ui/ressources/zoom.png") )) - logger.info(os.path.abspath(f"{file_path}/../ui/ressources/zoom.png")) - icons.append(("zoom",icon_zoom)) if "iso" in items: diff --git a/src/View/Tools/PamhyrWidget.py b/src/View/Tools/PamhyrWidget.py index 60e06174..22bf9584 100644 --- a/src/View/Tools/PamhyrWidget.py +++ b/src/View/Tools/PamhyrWidget.py @@ -26,10 +26,21 @@ from PyQt5.QtWidgets import ( QSpinBox, QTimeEdit, QDateTimeEdit, QItemDelegate, ) -from View.ASubWindow import AWidget +from View.Tools.ASubWindow import AWidget logger = logging.getLogger() +class PamhyrWidget(AWidget): + _pamhyr_ui = "" + _pamhyr_name = "PamhyrWidget" + + def __init__(self, parent=None): + super(PamhyrWidget, self).__init__( + ui = self._pamhyr_ui, + parent = parent + ) + + class PamhyrExtendedTimeEdit(AWidget): def __init__(self, parent=None): super(PamhyrExtendedTimeEdit, self).__init__( From 7c4ba10871ca4b3e49f0e84dec01abda458cfeaf Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 15:57:28 +0200 Subject: [PATCH 011/248] refactoring: Continue refacto using PamhyrWindow. --- src/View/LateralContribution/Edit/Table.py | 2 - src/View/LateralContribution/Edit/Window.py | 66 ++++++++------------- src/View/LateralContribution/Window.py | 42 +++++-------- src/View/MainWindow.py | 2 +- 4 files changed, 41 insertions(+), 71 deletions(-) diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index 2fff0895..8b43602d 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -22,8 +22,6 @@ from datetime import date, time, datetime, timedelta from tools import trace, timer -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow from View.Tools.PamhyrTable import PamhyrTableModel from PyQt5.QtCore import ( diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 53576a6b..f37c1759 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -18,8 +18,7 @@ from tools import timer, trace -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate from PyQt5.QtGui import ( @@ -47,44 +46,36 @@ from View.LateralContribution.Edit.Plot import Plot _translate = QCoreApplication.translate -class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit lateral contribution", - data=None, study=None, parent=None): +class EditLateralContributionWindow(PamhyrWindow): + _pamhyr_ui = "EditLateralContribution" + _pamhyr_name = "Edit lateral contribution" + + def __init__(self, data=None, + study=None, config=None, + parent=None): self._data = data - self._study = study - self._title = title - self.compute_title() - - super(EditLateralContributionWindow, self).__init__( - name=title, ui="EditLateralContribution", parent=parent - ) - - self.ui.setWindowTitle(self._title) - - self.setup_sc() - self.setup_table() - self.setup_plot() - self.setup_connections() - - def compute_title(self): + name = self._pamhyr_name if self._data is not None: edge_name = (self._data.edge.name if self._data.edge is not None else _translate("LateralContribution", "Not associate")) - self._title = ( - _translate("Edit lateral contribution", self._title) + - f" - {self._study.name} " + + name = ( + _translate("Edit lateral contribution", self._pamhyr_name) + + f" - {study.name} " + f" - {self._data.name} ({self._data.id}) " + f"({long_types[self._data.lctype]} - {edge_name})" ) - def setup_sc(self): - self._undo_stack = QUndoStack() + super(EditLateralContributionWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent + ) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self.setup_table() + self.setup_plot() + self.setup_connections() def setup_table(self): retranslate() @@ -133,17 +124,11 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self.update) def update(self): @@ -199,8 +184,7 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(row) self.plot.update() - - def copy(self): + def _copy(self): rows = self.index_selected_rows() table = [] @@ -212,7 +196,7 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -226,10 +210,10 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table.paste(row, header, data) self.plot.update() - def undo(self): + def _undo(self): self._table.undo() self.plot.update() - def redo(self): + def _redo(self): self._table.redo() self.plot.update() diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index 31f82906..2793ee36 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -66,32 +65,26 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class LateralContributionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Lateral contribution", study=None, parent=None): - self._title = title + " - " + study.name +class LateralContributionWindow(PamhyrWindow): + _pamhyr_ui = "LateralContributions" + _pamhyr_name = "Lateral contribution" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(LateralContributionWindow, self).__init__( - name=title, ui="LateralContributions", parent=parent + title = name, + study = study, + config = config, + parent=parent ) - self._study = study self._lcs = self._study.river.lateral_contribution - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() self._table = {} @@ -146,11 +139,6 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_edit").triggered.connect(self.edit) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - for t in ["liquid", "solid", "suspenssion"]: table = self.find(QTableView, f"tableView_{t}") table.selectionModel()\ @@ -247,20 +235,20 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table[tab].move_down(row) self._set_current_reach() - def copy(self): + def _copy(self): logger.info("TODO: copy") self._set_current_reach() - def paste(self): + def _paste(self): logger.info("TODO: paste") self._set_current_reach() - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() self._set_current_reach() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() self._set_current_reach() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index f701a474..02a4e1fe 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -46,7 +46,7 @@ from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow -# from View.LateralContribution.Window import LateralContributionWindow +from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow # from View.Frictions.Window import FrictionsWindow From b2b5c971c3b56751fd6817da64f66ae681893d04 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 16:38:14 +0200 Subject: [PATCH 012/248] refactoring: Apply new Pamhyr tools on IC. --- src/View/InitialConditions/DialogDischarge.py | 12 ++-- src/View/InitialConditions/DialogHeight.py | 14 +++-- src/View/InitialConditions/Window.py | 62 ++++++++----------- src/View/MainWindow.py | 3 +- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/View/InitialConditions/DialogDischarge.py b/src/View/InitialConditions/DialogDischarge.py index f8e7e32a..7bbe8abd 100644 --- a/src/View/InitialConditions/DialogDischarge.py +++ b/src/View/InitialConditions/DialogDischarge.py @@ -16,8 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -32,10 +31,15 @@ from PyQt5.QtWidgets import ( QDoubleSpinBox, ) -class DischargeDialog(ASubWindow, ListedSubWindow): +class DischargeDialog(PamhyrDialog): + _pamhyr_ui = "InitialConditions_Dialog_Generator_Discharge" + _pamhyr_name = "Discharge" + def __init__(self, title="Discharge", parent=None): super(DischargeDialog, self).__init__( - name=title, ui="InitialConditions_Dialog_Generator_Discharge", parent=parent + title = _pamhyr_name, + options = [], + parent = parent ) self.value = None diff --git a/src/View/InitialConditions/DialogHeight.py b/src/View/InitialConditions/DialogHeight.py index 18ad67c8..9ccadbd6 100644 --- a/src/View/InitialConditions/DialogHeight.py +++ b/src/View/InitialConditions/DialogHeight.py @@ -16,8 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -32,10 +31,15 @@ from PyQt5.QtWidgets import ( QDoubleSpinBox, ) -class HeightDialog(ASubWindow, ListedSubWindow): - def __init__(self, title="Height", parent=None): +class HeightDialog(PamhyrDialog): + _pamhyr_ui = "InitialConditions_Dialog_Generator_Height" + _pamhyr_name = "Height" + + def __init__(self, parent=None): super(HeightDialog, self).__init__( - name=title, ui="InitialConditions_Dialog_Generator_Height", parent=parent + title = name, + options = [], + parent = parent ) self.value = None diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 00e7b121..3f5ee1d8 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -61,41 +60,37 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Initial condition", - study=None, parent=None): - self._study = study - self._reach = study.river.current_reach() - self._ics = self._study.river.initial_conditions.get(self._reach) +class InitialConditionsWindow(PamhyrWindow): + _pamhyr_ui = "InitialConditions" + _pamhyr_name = "Initial condition" - self.setup_title(title) + def __init__(self, study=None, config=None, reach=None, parent=None): + if reach is not None: + self._reach = reach + else: + self._reach = study.river.current_reach() - super(InitialConditionsWindow, self).__init__( - name=self._title, ui="InitialConditions", parent=parent + name = ( + self._pamhyr_name + + " - " + study.name + + " - " + self._reach.name ) - self.setup_sc() + super(InitialConditionsWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent + ) + + self._ics = study.river.initial_conditions.get(self._reach) + self.setup_table() self.setup_graph() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() @@ -167,11 +162,6 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self.generate_discharge ) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self._update_plot) def index_selected_row(self): @@ -227,19 +217,19 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(row) self._update_plot() - def copy(self): + def _copy(self): logger.info("TODO: copy") self._update_plot() - def paste(self): + def _paste(self): logger.info("TODO: paste") self._update_plot() - def undo(self): + def _undo(self): self._table.undo() self._update_plot() - def redo(self): + def _redo(self): self._table.redo() self._update_plot() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 02a4e1fe..f5b3ffef 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -47,7 +47,7 @@ from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow -# from View.InitialConditions.Window import InitialConditionsWindow +from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow # from View.Frictions.Window import FrictionsWindow # from View.SedimentLayers.Window import SedimentLayersWindow @@ -611,6 +611,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if initial is None: initial = InitialConditionsWindow( study = self._study, + config = self.conf, parent = self ) initial.show() From 809a0dd9cbf051298c2e996ddf2568395ab8a077 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 17:01:59 +0200 Subject: [PATCH 013/248] refarctoring: Apply PamhyrWindow to frictions. --- src/View/Frictions/Window.py | 62 ++++++++++++++-------------------- src/View/MainWindow.py | 4 +-- src/View/Stricklers/Window.py | 45 ++++++++---------------- src/View/Tools/PamhyrWidget.py | 2 +- 4 files changed, 44 insertions(+), 69 deletions(-) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 060be330..2a547528 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -59,39 +58,35 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class FrictionsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Frictions", study=None, parent=None): - self._study = study - self._reach = self._study.river.current_reach() +class FrictionsWindow(PamhyrWindow): + _pamhyr_ui = "Frictions" + _pamhyr_name = "Edit frictions" + + def __init__(self, reach=None, study=None, config=None, parent=None): + if reach is not None: + self._reach = reach + else: + self._reach = study.river.current_reach() + self._frictions = self._reach.frictions - self.setup_title(title) - - super(FrictionsWindow, self).__init__( - name=title, ui="Frictions", parent=parent + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._reach.name + ) + + super(FrictionsWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) def setup_table(self): retranslate() @@ -156,11 +151,6 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_sort").triggered.connect(self.sort) self.find(QAction, "action_edit_stricklers").triggered.connect(self.edit_stricklers) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - table = self.find(QTableView, f"tableView") table.selectionModel()\ .selectionChanged\ @@ -234,16 +224,16 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): row = self.index_selected_row() self._table.move_down(row) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() def edit_stricklers(self): diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index f5b3ffef..562b05d7 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -48,8 +48,8 @@ from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow -# from View.Stricklers.Window import StricklersWindow -# from View.Frictions.Window import FrictionsWindow +from View.Stricklers.Window import StricklersWindow +from View.Frictions.Window import FrictionsWindow # from View.SedimentLayers.Window import SedimentLayersWindow # from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index 12620c4f..d9128d00 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -50,32 +49,23 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class StricklersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Stricklers", study=None, config=None, parent=None): - self._title = title + " - " + study.name +class StricklersWindow(PamhyrWindow): + _pamhyr_ui = "Stricklers" + _pamhyr_name = "Stricklers" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(StricklersWindow, self).__init__( - name=title, ui="Stricklers", parent=parent + title = name, + study = study, + config = config, + parent = parent ) - self._study = study - self._config = config - - self.setup_sc() self.setup_table() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_table(self): retranslate() self._table = {} @@ -105,11 +95,6 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def index_selected_rows(self): table = self.find(QTableView, f"tableView_study") return list( @@ -139,14 +124,14 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): def sort(self): self._table['study'].sort(False) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table['study'].undo() - def redo(self): + def _redo(self): self._table['study'].redo() diff --git a/src/View/Tools/PamhyrWidget.py b/src/View/Tools/PamhyrWidget.py index 22bf9584..8c85b1c1 100644 --- a/src/View/Tools/PamhyrWidget.py +++ b/src/View/Tools/PamhyrWidget.py @@ -31,7 +31,7 @@ from View.Tools.ASubWindow import AWidget logger = logging.getLogger() class PamhyrWidget(AWidget): - _pamhyr_ui = "" + _pamhyr_ui = None _pamhyr_name = "PamhyrWidget" def __init__(self, parent=None): From 1a36d78ee29233e8c572f5c6284a913d3da0f2b2 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 09:34:26 +0200 Subject: [PATCH 014/248] refactoring: Apply PamhyrWindow to SL. --- src/View/MainWindow.py | 4 +- src/View/SedimentLayers/Edit/Window.py | 59 +++++++---------- .../SedimentLayers/Reach/Profile/Window.py | 62 +++++++----------- src/View/SedimentLayers/Reach/SLDialog.py | 15 +++-- src/View/SedimentLayers/Reach/Window.py | 57 +++++++---------- src/View/SedimentLayers/Window.py | 64 ++++++++++--------- src/View/ui/ProfileSedimentLayers.ui | 14 ---- 7 files changed, 114 insertions(+), 161 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 562b05d7..002ee11b 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -50,8 +50,8 @@ from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow -# from View.SedimentLayers.Window import SedimentLayersWindow -# from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow +from View.SedimentLayers.Window import SedimentLayersWindow +from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow # from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow # from View.CheckList.Window import CheckListWindow diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index 29b9ddff..ebe3aece 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -52,41 +51,32 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit Sediment Layers", - study=None, sl=None, parent=None): - self._study = study +class EditSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "EditSedimentLayers" + _pamhyr_name = "Edit Sediment Layers" + + def __init__(self, study=None, config = None, + sl=None, parent=None): self._sl = sl - self.setup_title(title) - - super(EditSedimentLayersWindow, self).__init__( - name=self._title, ui="EditSedimentLayers", parent=parent - ) - - self.setup_sc() - self.setup_table() - self.setup_graph() - self.setup_connections() - - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): name = self._sl.name if name == "": name = _translate("SedimentLayers", "(no name)") - self._title = ( - title + " - " + self._study.name + " - " + name + name = ( + self._pamhyr_name + " - " + study.name + " - " + name ) - def setup_sc(self): - self._undo_stack = QUndoStack() + super(EditSedimentLayersWindow, self).__init__( + title = name, + study = study, + config = config, + parent=parent + ) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self.setup_table() + self.setup_graph() + self.setup_connections() def setup_table(self): retranslate() @@ -131,11 +121,6 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_move_up").triggered.connect(self.move_up) self.find(QAction, "action_move_down").triggered.connect(self.move_down) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self._set_plot) self._table.layoutChanged.connect(self._set_plot) @@ -179,14 +164,14 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(rows[0]) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index eaae59d3..f424e06a 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -4,8 +4,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -38,27 +37,31 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Profile sediment layers", study=None, profile=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers - self._profile = profile - self._reach = self._study.river.current_reach().reach +class ProfileSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "ProfileSedimentLayers" + _pamhyr_name = "Profile sediment layers" - self.setup_title(title) + def __init__(self, study=None, config=None, profile=None, parent=None): + self._sediment_layers = study.river.sediment_layers + self._profile = profile + self._reach = profile.reach + + name = self.compute_name(study) super(ProfileSedimentLayersWindow, self).__init__( - name=self._title, ui="ProfileSedimentLayers", parent=parent + title = name, + study = study, + config = config, + parent=parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_title(self, title): + def compute_name(self, study): rname = self._reach.name if rname == "": rname = _translate("SedimentLayers", "(no name)") @@ -70,20 +73,12 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): "(no name - @kp)").replace("@kp", str(self._profile.kp) ) - self._title = ( - title + " - " - + self._study.name + " - " + return ( + self._pamhyr_name + " - " + + study.name + " - " + rname + " - " + pname ) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() @@ -130,18 +125,9 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): ) self.plot.draw() - def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - - self._table.layoutChanged\ - .connect(self._update_plot) - self._table.dataChanged\ - .connect(self._update_plot) - + self._table.layoutChanged.connect(self._update_plot) + self._table.dataChanged.connect(self._update_plot) def index_selected_rows(self): table = self.find(QTableView, f"tableView") @@ -155,14 +141,14 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): ) ) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() diff --git a/src/View/SedimentLayers/Reach/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py index ce1e9794..c8a1efa0 100644 --- a/src/View/SedimentLayers/Reach/SLDialog.py +++ b/src/View/SedimentLayers/Reach/SLDialog.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -20,12 +19,16 @@ from View.SedimentLayers.Reach.translate import * _translate = QCoreApplication.translate -class SLDialog(ASubWindow, ListedSubWindow): - def __init__(self, title="SL", study=None, parent=None): - self._study = study +class SLDialog(PamhyrDialog): + _pamhyr_ui = "SLDialog" + _pamhyr_name = "SL" + def __init__(self, study=None, config=None, parent=None): super(SLDialog, self).__init__( - name=title, ui="SLDialog", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + parent=parent ) self.setup_combobox() diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index d79312b2..2a655b7c 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -4,8 +4,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -40,38 +39,31 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Reach sediment layers", study=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers - self._reach = self._study.river.current_reach().reach +class ReachSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "ReachSedimentLayers" + _pamhyr_name = "Reach sediment layers" - self.setup_title(title) + def __init__(self, study=None, config=None, parent=None): + self._sediment_layers = study.river.sediment_layers + self._reach = study.river.current_reach().reach - super(ReachSedimentLayersWindow, self).__init__( - name=self._title, ui="ReachSedimentLayers", parent=parent + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._reach.name + ) + + super(ReachSedimentLayersWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " + self._study.name + " - " + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() @@ -129,11 +121,6 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): .clicked\ .connect(self.apply_sl_each_profile) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.layoutChanged\ .connect(self._update_plot) self._table.dataChanged\ @@ -152,17 +139,17 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): ) ) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() self._update_plot() - def redo(self): + def _redo(self): self._table.redo() self._update_plot() diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index 6c14ae6c..c4010745 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -1,11 +1,26 @@ +# 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 . + # -*- coding: utf-8 -*- import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -38,37 +53,33 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="SedimentLayersList", study=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers +class SedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "SedimentLayersList" + _pamhyr_name = "Sediment Layers List" - self.setup_title(title) + def __init__(self, study=None, config=None, parent=None): + self._sediment_layers = study.river.sediment_layers - super(SedimentLayersWindow, self).__init__( - name=self._title, ui="SedimentLayersList", parent=parent + name = ( + self._pamhyr_name + " - " + study.name + ) + + super(SedimentLayersWindow, self).__init__( + title = name, + study = study, + config = config, + parent=parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - def setup_title(self, title): self._title = ( title + " - " + self._study.name ) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() @@ -94,11 +105,6 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_edit").triggered.connect(self.edit_sediment_layers) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - table = self.find(QTableView, f"tableView") table.selectionModel()\ .selectionChanged\ @@ -148,16 +154,16 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self._table.delete(rows) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() def edit_sediment_layers(self): diff --git a/src/View/ui/ProfileSedimentLayers.ui b/src/View/ui/ProfileSedimentLayers.ui index b9f5106d..05f6618e 100644 --- a/src/View/ui/ProfileSedimentLayers.ui +++ b/src/View/ui/ProfileSedimentLayers.ui @@ -39,20 +39,6 @@ - - - toolBar - - - TopToolBarArea - - - false - - - - - From ca10f19fe53990493f9adac8d14a88ecab75c647 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 12:03:41 +0200 Subject: [PATCH 015/248] refactoring: Continue apply PamhyrWindow. --- src/View/CheckList/Window.py | 28 ++++++++-------- src/View/MainWindow.py | 8 ++--- src/View/Results/Window.py | 51 +++++++++++++---------------- src/View/RunSolver/Log/Window.py | 24 +++++++------- src/View/RunSolver/Window.py | 42 +++++++++++------------- src/View/SolverParameters/Window.py | 43 +++++++++++------------- 6 files changed, 92 insertions(+), 104 deletions(-) diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index 4c40ad1f..b02522bc 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -18,8 +18,7 @@ from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -44,30 +43,31 @@ from View.CheckList.Worker import Worker _translate = QCoreApplication.translate -class CheckListWindow(ASubMainWindow, ListedSubWindow): +class CheckListWindow(PamhyrWindow): + _pamhyr_ui = "CheckList" + _pamhyr_name = "Check list" signalStatus = pyqtSignal(str) - def __init__(self, title="Check list", - autorun:bool=True, + def __init__(self, autorun:bool=True, study=None, config=None, solver=None, parent=None): - self._title = title + " - " + study.name - self._autorun = autorun - - self._study = study - self._config = config self._solver = solver - self._parent = parent + + name = self._pamhyr_name + " - " + study.name super(CheckListWindow, self).__init__( - name=self._title, ui="CheckList", parent=parent + title = name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self._checker_list = ( - self._study.checkers() + self._solver.checkers() + self._study.checkers() + + self._solver.checkers() ) self.setup_table() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 002ee11b..01e7a3b0 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -52,10 +52,10 @@ from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow -# from View.SolverParameters.Window import SolverParametersWindow -# from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -# from View.CheckList.Window import CheckListWindow -# from View.Results.Window import ResultsWindow +from View.SolverParameters.Window import SolverParametersWindow +from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow +from View.CheckList.Window import CheckListWindow +from View.Results.Window import ResultsWindow # from View.Debug.Window import ReplWindow from Model.Study import Study diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 251f5a13..4009c836 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -21,8 +21,7 @@ from datetime import datetime from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -63,23 +62,32 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class ResultsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Results", - study=None, solver=None, results=None, +class ResultsWindow(PamhyrWindow): + _pamhyr_ui = "Results" + _pamhyr_name = "Results" + + def __init__(self, study=None, config=None, + solver=None, results=None, parent=None): - self._study = study self._solver = solver self._results = results self._timestamps = sorted(list(self._results.get("timestamps"))) - self.setup_title(title) - - super(ResultsWindow, self).__init__( - name=title, ui="Results", parent=parent + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._solver.name + " - " + + self._results.date + ) + + super(ResultsWindow, self).__init__( + title = name, + study = study, + config = config, + parent=parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_slider() @@ -96,14 +104,6 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): + self._results.date ) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() self._table = {} @@ -289,11 +289,6 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._status_label.setText(txt) def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - fun = { "reach": self._set_current_reach, "profile": self._set_current_profile, @@ -411,14 +406,14 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): timestamp = self._timestamps[self._slider_time.value()] self.update(timestamp = timestamp) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() diff --git a/src/View/RunSolver/Log/Window.py b/src/View/RunSolver/Log/Window.py index 93d42165..2a33311f 100644 --- a/src/View/RunSolver/Log/Window.py +++ b/src/View/RunSolver/Log/Window.py @@ -22,8 +22,7 @@ import os from queue import Queue from tools import trace, timer -from View.ASubWindow import ASubWindow, ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog, PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -45,24 +44,23 @@ from PyQt5.QtWidgets import ( _translate = QCoreApplication.translate -class SolverLogFileWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver logs", - file_name = None, +class SolverLogFileWindow(PamhyrWindow): + _pamhyr_ui = "SolverLogFile" + _pamhyr_name = "Solver logs" + + def __init__(self, file_name = None, study=None, config=None, solver=None, parent=None): - self._title = title - self._parent = parent - - self._study = study - self._config = config self._solver = solver - self._file_name = file_name super(SolverLogFileWindow, self).__init__( - name=self._title, ui="SolverLogFile", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self.setup_action() self.setup_connections() diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 226d4da5..72a65d45 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -23,8 +23,7 @@ import tempfile from queue import Queue from tools import trace, timer -from View.ASubWindow import ASubWindow, ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog, PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -56,20 +55,21 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class SelectSolverWindow(ASubWindow, ListedSubWindow): - def __init__(self, title="Select solver", - study=None, config=None, - parent=None): - self._title = title +class SelectSolverWindow(PamhyrDialog): + _pamhyr_ui = "SelectSolver" + _pamhyr_name = "Select solver" - self._study = study - self._config = config + def __init__(self, study=None, config=None, + parent=None): self._solver = None super(SelectSolverWindow, self).__init__( - name=self._title, ui="SelectSolver", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self.setup_combobox() self.setup_connections() @@ -101,24 +101,22 @@ class SelectSolverWindow(ASubWindow, ListedSubWindow): super(SelectSolverWindow, self).accept() +class SolverLogWindow(PamhyrWindow): + _pamhyr_ui = "SolverLog" + _pamhyr_name = "Solver Log" -class SolverLogWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver logs", - study=None, config=None, + def __init__(self, study=None, config=None, solver=None, parent=None): - self._title = title - self._parent = parent - - self._study = study - self._config = config self._solver = solver - self._results = None super(SolverLogWindow, self).__init__( - name=self._title, ui="SolverLog", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self.setup_action() self.setup_alarm() diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index c04eebc7..a3b225a6 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -51,23 +50,28 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class SolverParametersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver parameters", study=None, parent=None): +class SolverParametersWindow(PamhyrWindow): + _pamhyr_ui = "SolverParameters" + _pamhyr_name = "Solver parameters" + + def __init__(self, study=None, config=None, parent=None): # Init tanslate dictionary tr.init() - self._title = title + " - " + study.name + name = self._pamhyr_name + " - " + study.name super(SolverParametersWindow, self).__init__( - name=title, ui="SolverParameters", parent=parent + title = name, + study = study, + config = config, + options = ['copy'], + parent = parent ) - self._study = study self._params = self._study.river.parameters self.setup_sc() self.setup_table() - self.setup_connections() self.ui.setWindowTitle(self._title) @@ -77,10 +81,10 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): for st in solver_type_list: self._undo_stack[st] = QUndoStack() - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self._undo_sc = QShortcut(QKeySequence.Undo, self) + self._redo_sc = QShortcut(QKeySequence.Redo, self) + self._undo_sc.activated.connect(self._undo) + self._redo_sc.activated.connect(self._redo) def setup_table(self): self._table = {} @@ -115,31 +119,24 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): # Create tab self._tab_widget.addTab(widget, f"{solver_long_name[st]}") - - def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def current_tab(self): return self.find(QTabWidget, "tabWidget")\ .currentWidget()\ .objectName()\ .replace("tab_", "") - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() self._set_current_reach() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() self._set_current_reach() - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") From 624e9b6f71b72ab208b960efc25a6c5c02f4575a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 12:07:10 +0200 Subject: [PATCH 016/248] refactoring: Apply PamhyrWindow on Debug window. --- src/View/Debug/Window.py | 24 +++++++++++------------- src/View/MainWindow.py | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py index 9b14aa20..2e99dadb 100644 --- a/src/View/Debug/Window.py +++ b/src/View/Debug/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -45,20 +44,19 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class ReplWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Debug REPL", - study=None, config=None, +class ReplWindow(PamhyrWindow): + _pamhyr_ui = "DebugRepl" + _pamhyr_name = "Debug REPL" + + def __init__(self, study=None, config=None, solver=None, parent=None): - self._title = title - - self._study = study - self._config = config - self._parent = parent - super(ReplWindow, self).__init__( - name=self._title, ui="DebugRepl", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self.__debug_exec_result__ = None self._history = [] diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 01e7a3b0..cb9d8b7e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -56,7 +56,7 @@ from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow from View.Results.Window import ResultsWindow -# from View.Debug.Window import ReplWindow +from View.Debug.Window import ReplWindow from Model.Study import Study From e7fd2077bd3f7c390488fc352df98205026a5d75 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 14:19:22 +0200 Subject: [PATCH 017/248] doc: dev: Update with PamhyrWindow, and minor change in hash computation. --- doc/dev/documentation.org | 40 +++++++++++++++++-------------- src/View/Tools/ListedSubWindow.py | 7 ++++-- src/View/Tools/PamhyrWindow.py | 13 ++++------ 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 16d6a69c..c5266896 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -445,33 +445,37 @@ window, and =/src/View/ui/Widgets= for custom widget. *** Window -An abstract class ASubMainWindow is used for most of Pamhyr2 -windows. This class implemente some useful method allows to easy load -an window UI file. In addition, the abstract ListedSubWindow must be -also inherits by a window class, it is allow to keep a list of open -windows and avoid window duplicate. +The abstract class PamhyrWindow and PamhyrDialog are used for most of +Pamhyr2 window. These class allow to create an window for Pamhyr2 GUI +and implemente some useful methods. #+NAME: window #+CAPTION: Example of Pamhyr2 window #+begin_src python :python python3 :results output :noweb yes - class MyWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="My window", - study=None, config=None, parent=None): - self._study = study - self._config = config - self._parent = parent + from View.Tools.PamhyrWindow import PamhyrWindow - self._title = title + " - " + study.name + class MyWindow(PamhyrWindow): + _pamhyr_ui = "MyUI" + _pamhyr_name = "My window" + + def __init__(self, my_data=None, + study=None, config=None, + parent=None): + self._my_data = my_data super(MyWindow, self).__init__( - name=title, # The windows name for windows - # registration (avoid duplicate) - ui="My", # The ui file name (here "My.ui") - parent=parent # The parent window + # Window title + title = self._pamhyr_name + " - " + study.name, + # Window standard data + study = study, config = config, parent = parent, + # Activate undo/redo and copy/paste shortcut + options = ["undo", "copy"] ) - # Set Windows title - self.ui.setWindowTitle(self._title) + # Add custom data to hash window computation + self._hash_data.append(self._my_data) + + # Setup custom window components self.setup_table() self.setup_connections() diff --git a/src/View/Tools/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py index f671cc60..6c93b84e 100644 --- a/src/View/Tools/ListedSubWindow.py +++ b/src/View/Tools/ListedSubWindow.py @@ -36,7 +36,10 @@ class ListedSubWindow(object): def sub_win_add(self, name, win): self.sub_win_list.append((name, win)) self.sub_win_cnt += 1 - logger.info(f"Open window: {name} ({self.sub_win_cnt})") + try: + logger.info(f"Open window: {name}: {self.sub_win_cnt}: {win.hash()}") + except: + logger.info(f"Open window: {name}: {self.sub_win_cnt}: X") def sub_win_del(self, name): self.sub_win_list = list( @@ -46,7 +49,7 @@ class ListedSubWindow(object): ) ) self.sub_win_cnt = len(self.sub_win_list) - logger.info(f"Close window: {name} ({self.sub_win_cnt})") + logger.info(f"Close window: {name}: {self.sub_win_cnt}") def _sub_win_exists(self, name): return reduce( diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py index d6ae6752..34ff547f 100644 --- a/src/View/Tools/PamhyrWindow.py +++ b/src/View/Tools/PamhyrWindow.py @@ -35,6 +35,7 @@ class PamhyrWindowTools(object): def __init__(self, options = ["undo", "copy"], parent = None, **kwargs): super(PamhyrWindowTools, self).__init__() + self._hash_data = [] self._undo_stack = None if "undo" in options: @@ -130,12 +131,7 @@ class PamhyrWindowTools(object): Returns: The hash """ - data = [ - self._study, - self._config, - ] - - return self._hash() + return self._hash(self._hash_data) class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): @@ -152,14 +148,15 @@ class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): self._config = config self._parent = parent - logger.info(self._pamhyr_name) - super(PamhyrWindow, self).__init__( name = self._pamhyr_name, ui = self._pamhyr_ui, parent = parent, ) + self._hash_data.append(self._study) + self._hash_data.append(self._config) + self._set_title() From 2e94348bb8026fff36d625a1ddbbfcbeba2a85d5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 17:16:53 +0200 Subject: [PATCH 018/248] refactoring: Add PamhyrTranslate class for GUI translation and document it. --- doc/dev/documentation.org | 103 ++++++++++++++++++------------ src/View/Network/Window.py | 14 ++-- src/View/Network/translate.py | 30 ++++----- src/View/Tools/PamhyrTranslate.py | 56 ++++++++++++++++ src/View/Tools/PamhyrWindow.py | 12 ++-- 5 files changed, 145 insertions(+), 70 deletions(-) create mode 100644 src/View/Tools/PamhyrTranslate.py diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index c5266896..ed4bc222 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -443,49 +443,83 @@ than hand made windows and widget creation, and saves us some purely descriptive code. The UI files are saved into =src/View/ui= for window, and =/src/View/ui/Widgets= for custom widget. +*** Translate + +#+NAME: pamhyr-trad +#+CAPTION: Example of =PamhyrTranslate= class implementation with a global traduction for /FooBar/ and a additional dictionary =table_headers= +#+begin_src python :python python3 :results output :noweb yes + from PyQt5.QtCore import QCoreApplication + from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate + + class MyTranslate(PamhyrTranslate): + def __init__(self): + super(MyTranslate, self).__init__() + + # Add traduction to global dictionary + self._dict["My"] = _translate("My", "FooBar") + # Add an additional translate dictionary + self._sub_dict["table_headers"] = { + "foo": _translate("My", "Foo"), + "bar": _translate("My", "Bar"), + "baz": _translate("My", "Baz"), + } +#+end_src + *** Window The abstract class PamhyrWindow and PamhyrDialog are used for most of Pamhyr2 window. These class allow to create an window for Pamhyr2 GUI -and implemente some useful methods. +and implemente some useful methods. The super class method difine some +generic value from optional parameters, for examples: +- =self._study=: The study giving in constructor parameters =study= + (typically a =Model.Study= class object) +- =self._config=: The configuration giving in constructor parameters + =config= (typically a =Config= class object) +- =self._trad=: The traductor dictionary giving in constructor + parameters =trad= (typically a =Model.Tools.PamhyrTranslate= class + object) #+NAME: window #+CAPTION: Example of Pamhyr2 window #+begin_src python :python python3 :results output :noweb yes - from View.Tools.PamhyrWindow import PamhyrWindow + from View.Tools.PamhyrWindow import PamhyrWindow + from View.My.Translate import MyTranslate - class MyWindow(PamhyrWindow): - _pamhyr_ui = "MyUI" - _pamhyr_name = "My window" + class MyWindow(PamhyrWindow): + _pamhyr_ui = "MyUI" + _pamhyr_name = "My window" - def __init__(self, my_data=None, - study=None, config=None, - parent=None): - self._my_data = my_data + def __init__(self, my_data=None, + study=None, config=None, + parent=None): + self._my_data = my_data - super(MyWindow, self).__init__( - # Window title - title = self._pamhyr_name + " - " + study.name, - # Window standard data - study = study, config = config, parent = parent, - # Activate undo/redo and copy/paste shortcut - options = ["undo", "copy"] - ) + super(MyWindow, self).__init__( + # Window title + title = self._pamhyr_name + " - " + study.name, + # Window standard data + study = study, config = config, + trad = MyTranslate(), + parent = parent, + # Activate undo/redo and copy/paste shortcut + options = ["undo", "copy"] + ) - # Add custom data to hash window computation - self._hash_data.append(self._my_data) + # Add custom data to hash window computation + self._hash_data.append(self._my_data) - # Setup custom window components - self.setup_table() - self.setup_connections() + # Setup custom window components + self.setup_table() + self.setup_connections() - def setup_table(self): - # Init table(s)... + def setup_table(self): + # Init table(s)... - def setup_connections(self): - # Init action connection(s)... + def setup_connections(self): + # Init action connection(s)... - # ... + # ... #+end_src Typically we called method =setup_*=, the method to initialize some @@ -499,20 +533,6 @@ and =setData= methode to implement, but the constructor needs more information than a classic QAbstractTableModel class. #+begin_src python :python python3 :results output :noweb yes - # Table headers with display name translatable (translate.py) - table_headers = { - "foo": _translate("My", "Foo"), - "bar": _translate("My", "Bar"), - "baz": _translate("My", "Baz"), - } - - def retranslate(): - # Hack because dict definition is not able to be - # translate... (need to be called in window '__init__') - table_headers["foo"] = _translate("My", "Foo") - table_headers["bar"] = _translate("My", "Bar") - table_headers["baz"] = _translate("My", "Baz") - # Table model definition (Table.py) class MyTableModel(PamhyrTableModel): def data(self, index, role): @@ -523,6 +543,7 @@ information than a classic QAbstractTableModel class. # Set VALUE at INDEX... # Table model creation (Window.py) + table_headers = self._trad.get_dict("table_headers") self._model = MyTableModel( table_view = table, # The table view object table_headers = table_headers, # The table column headers dict diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index a54c0527..f0458596 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -41,10 +41,7 @@ from Model.River import RiverNode, RiverReach, River from View.Tools.PamhyrWindow import PamhyrWindow from View.Network.GraphWidget import GraphWidget from View.Network.UndoCommand import * -from View.Network.translate import ( - table_headers_node, table_headers_edge, - retranslate, -) +from View.Network.translate import NetworkTranslate from View.Network.Table import ( ComboBoxDelegate, NodeTableModel, EdgeTableModel, ) @@ -63,24 +60,25 @@ class NetworkWindow(PamhyrWindow): title = self._pamhyr_name + " - " + study.name, study = study, config = config, + trad = NetworkTranslate(), options = ['undo'], parent=parent, ) self._graph = study.river + self._table_headers_node = self._trad.get_dict("table_headers_node") + self._table_headers_edge = self._trad.get_dict("table_headers_edge") self.setup_graph() self.setup_table() self.setup_connections() def setup_table(self): - retranslate() - # Nodes table table = self.find(QTableView, "tableView_nodes") self._nodes_model = NodeTableModel( table_view = table, - table_headers = table_headers_node, + table_headers = self._table_headers_node, editable_headers = ["name"], data = self._graph, undo = self._undo_stack, @@ -99,7 +97,7 @@ class NetworkWindow(PamhyrWindow): self._reachs_model = EdgeTableModel( table_view = table, - table_headers = table_headers_edge, + table_headers = self._table_headers_edge, editable_headers = ["name", "node1", "node2"], delegates = { "node1": self.delegate_combobox, diff --git a/src/View/Network/translate.py b/src/View/Network/translate.py index 24935e1b..44e97cf5 100644 --- a/src/View/Network/translate.py +++ b/src/View/Network/translate.py @@ -18,23 +18,21 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers_node = { - "name": _translate("Network", "Name"), - "type": _translate("Network", "Type"), -} +class NetworkTranslate(PamhyrTranslate): + def __init__(self): + super(NetworkTranslate, self).__init__() -table_headers_edge = { - "name": _translate("Network", "Name"), - "node1": _translate("Network", "Source node"), - "node2": _translate("Network", "Destination node"), -} + self._sub_dict["table_headers_node"] = { + "name": _translate("Network", "Name"), + "type": _translate("Network", "Type"), + } -def retranslate(): - table_headers_node["name"] = _translate("Network", "Name") - table_headers_node["type"] = _translate("Network", "Type") - - table_headers_edge["name"] = _translate("Network", "Name") - table_headers_edge["node1"] = _translate("Network", "Source node") - table_headers_edge["node2"] = _translate("Network", "Destination node") + self._sub_dict["table_headers_edge"] = { + "name": _translate("Network", "Name"), + "node1": _translate("Network", "Source node"), + "node2": _translate("Network", "Destination node"), + } diff --git a/src/View/Tools/PamhyrTranslate.py b/src/View/Tools/PamhyrTranslate.py new file mode 100644 index 00000000..83c8da64 --- /dev/null +++ b/src/View/Tools/PamhyrTranslate.py @@ -0,0 +1,56 @@ +# PamhyrTranslate.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 . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + + +class PamhyrTranslate(object): + def __init__(self): + # Module global dictionnary + self._dict = {} + # Module sub dictionnary + self._sub_dict = {} + + def __getitem__(self, index): + if index not in self._dict: + return None + return self._dict[index] + + def _get_in_sub_dict(self, dictionary, index): + if dictionary not in self._sub_dict: + return None + return self._sub_dict[index] + + def get(self, dictionary, index): + if dictionary == "global": + return self[index] + + return self._get_in_sub_dict(dictionary, index) + + def get_dict(self, dictionary): + _dict = None + + if dictionary == "global": + _dict = self._dict + + if dictionary in self._sub_dict: + _dict = self._sub_dict[dictionary] + + return _dict diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py index 34ff547f..fb4bf1a1 100644 --- a/src/View/Tools/PamhyrWindow.py +++ b/src/View/Tools/PamhyrWindow.py @@ -140,12 +140,13 @@ class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): def __init__(self, title = "Pamhyr2", - study = None, config = None, + study = None, config = None, trad = None, options = ["undo", "copy"], parent = None): self._title = title self._study = study self._config = config + self._trad = trad self._parent = parent super(PamhyrWindow, self).__init__( @@ -166,21 +167,22 @@ class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools): def __init__(self, title = "Pamhyr2", - study = None, config = None, + study = None, config = None, trad = None, options = ["undo", "copy"], parent = None): self._title = title self._study = study self._config = config + self._trad = trad self._parent = parent - logger.info(self._pamhyr_name) - logger.info(self._pamhyr_ui) - super(PamhyrDialog, self).__init__( name = self._pamhyr_name, ui = self._pamhyr_ui, parent = parent, ) + self._hash_data.append(self._study) + self._hash_data.append(self._config) + self._set_title() From f93f3fe12278b05d904cc6e2745dc3b6ed1bb743 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 09:00:18 +0200 Subject: [PATCH 019/248] doc: dev: Update Table and UndoCommand. --- doc/dev/documentation.org | 52 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index ed4bc222..f12426b2 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -485,6 +485,7 @@ generic value from optional parameters, for examples: #+begin_src python :python python3 :results output :noweb yes from View.Tools.PamhyrWindow import PamhyrWindow from View.My.Translate import MyTranslate + from View.My.Table import MyTableModel class MyWindow(PamhyrWindow): _pamhyr_ui = "MyUI" @@ -532,8 +533,11 @@ QAbstractTableModel shortly. In simple cases, there are only =data= and =setData= methode to implement, but the constructor needs more information than a classic QAbstractTableModel class. +#+NAME: table-src +#+CAPTION: Definition of a table model from =PamhyrTableModel= in a file =View/My/Table.py=. #+begin_src python :python python3 :results output :noweb yes - # Table model definition (Table.py) + from View.Tools.PamhyrTable import PamhyrTableModel + class MyTableModel(PamhyrTableModel): def data(self, index, role): # Retrun data at INDEX... @@ -541,8 +545,11 @@ information than a classic QAbstractTableModel class. @pyqtSlot() def setData(self, index, value, role=Qt.EditRole): # Set VALUE at INDEX... +#+end_src - # Table model creation (Window.py) +#+CAPTION: Using the table model defined in Listing [[table-src]] in window funtion =setup_table= defined Listing [[window]]. +#+begin_src python :python python3 :results output :noweb yes + # Table model creation (Window.py: setup_table) table_headers = self._trad.get_dict("table_headers") self._model = MyTableModel( table_view = table, # The table view object @@ -553,11 +560,48 @@ information than a classic QAbstractTableModel class. "bar": self.my_delegate, # Custom delegate for column 'bar' }, data = self._my_lst, # The data - undo = self._undo_stack, # The undo command stack + undo = self._undo_stack, # The window undo command stack + ) +#+end_src + +*** UndoCommand + +All model modification must be done by an QUndoCommand, this command +allow to undo and redo an action. This a Qt class wi can inherit to +define custom undo command (see example Listing [[undo-cmd]]) + +#+NAME: undo-cmd +#+CAPTION: Example of custom UndoCommand, this command allow to add a node to graph in river network window (method redo), and delete it to graph with undo method +#+begin_src python :python python3 :results output :noweb yes +class AddNodeCommand(QUndoCommand): + def __init__(self, graph, node): + QUndoCommand.__init__(self) + + self._graph = graph + self._node = node + + def undo(self): + self._graph.remove_node(self._node.name) + + def redo(self): + self._graph.insert_node(self._node) +#+end_src + +All undo command must be push into a =QUndoStack= (see Listing +[[undo-cmd-push]]) to perform the action and allow user undo and redo this +action. In PamhyrWindow (and PamhyrDialog) the undo stack is +automatically create if the option ="undo"= is activate at window +creation, this stack is accessible at =self._undo_stack=. + +#+begin_src python :python python3 :results output :noweb yes + self._undo_stack.push( + AddNodeCommand( + self._graph, + node + ) ) #+end_src -*** TODO UndoCommand *** TODO Plot *** TODO Mainwindow ** TODO Solver From a81564e582d46657aa7025a1a02239bc707b7fa6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 09:20:17 +0200 Subject: [PATCH 020/248] doc: dev: Write Plot section. --- doc/dev/documentation.org | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index f12426b2..6ec31a3c 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -602,8 +602,35 @@ creation, this stack is accessible at =self._undo_stack=. ) #+end_src -*** TODO Plot -*** TODO Mainwindow +*** Plot + +To define a new plot you can create a class who inherit to APlot. The +creator need threee argument: + - A =canvas= of type =MplCanvas= + - A =data= used in =draw= and =update= to create and update the plot + - A optional =toolbar= of type =PamhyrToolbar= +This class must implement two method =draw= and =update=, the first +method to draw the plot from scratch, the second to update the plot if +data has changed. + +#+begin_src python :python python3 :results output :noweb yes + from View.Plot.APlot import APlot + + class MyPlot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None): + super(MyPlot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + def draw(self): + # Draw function code... + + def update(self, ind = None): + # Update function code... +#+end_src + ** TODO Solver #+name: graph-pipeline From ab2e307e3d2d860f4a80a4f76f47301eae67b691 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 12:10:47 +0200 Subject: [PATCH 021/248] doc: dev: Update graphs. --- doc/dev/documentation.org | 265 ++++++++++++++++++++++++++++++++++---- 1 file changed, 242 insertions(+), 23 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 6ec31a3c..cb40b018 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -97,35 +97,68 @@ simulation results to model data. #+name: graph-architecture #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="9cm", data=*this*, name="graph-architecture", caption="Pamhyr2 Model/View architecture scheme (inspired by Qt Model/View architecture [[https://doc.qt.io/qt-5/model-view-programming.html]])", float="t") +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-architecture", caption="Pamhyr2 Model/View architecture scheme (inspired by Qt Model/View architecture [[https://doc.qt.io/qt-5/model-view-programming.html]])", float="t") #+begin_src dot :file "images/graph-architecture.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=white]; + edge[colorscheme=set19,color=0]; + + root[style=invis]; + + subgraph cluster0 { + label="File System" + style=dashed; + + save[label="Pamhyr save",fillcolor="9",shape=note]; + sbin[label="Solver binary",fillcolor="9",shape=note]; + configfile[label="Pamhyr configuration file",fillcolor="9",shape=note]; + } - save[label="Pamhyr save",fillcolor="9",shape=note]; - model[label="Model",fillcolor="2"]; - view[label="View",fillcolor="1"]; - delegate[label="Delegate",fillcolor="3"]; - action[label="Action",fillcolor="3"]; - solver[label="Solver",fillcolor="6"]; - undocommand[label="Command",fillcolor="4"]; user[label="User",shape=ellipse]; - model -> save[label="Save"]; - save -> model[label="Load"]; + subgraph cluster1 { + label="Pamhyr2"; + + config[label="Configuration",fillcolor="5"]; + model[label="Model",fillcolor="2"]; + view[label="View",fillcolor="1"]; + delegate[label="Delegate",fillcolor="3"]; + action[label="Action",fillcolor="3"]; + solver[label="Solver",fillcolor="6"]; + undocommand[label="Command",fillcolor="4"]; + } + + root -> model[style=invis]; + root -> config[style=invis]; + + model -> save[dir=both,label="Save/Load"]; + config -> configfile[dir=both,label="Save/Load"]; + + undocommand -> solver[style=invis]; + action -> solver[style=invis]; + delegate -> solver[style=invis]; model -> view[label="Rendering"]; view -> delegate[label="Rendering"]; delegate -> undocommand[label="Create"]; - action -> undocommand[label="Create"]; + action -> undocommand[label="Create/use"]; action -> solver[label="Run"]; - solver -> model[label="Add results"]; + solver -> model[dir=both,label="Export/Results",labelangle=0,labelfloat=true,constraint=true]; + solver -> sbin[dir=both,label="Execute/Results"]; undocommand -> model[label="Modify"]; view -> user[label="Vizualize"]; user -> delegate[label="Modify"]; - user -> action[label="Activate"]; + user -> action[label="Triggere"]; + + config -> solver[label="Create",style=dashed,labelfloat=true,constraint=true]; + //model -> config[style=invis]; + config -> model[style=invis]; + delegate -> action[style=invis]; + save -> configfile[style=invis]; + root -> solver[style=invis]; + user -> solver[style=invis]; } #+end_src @@ -134,6 +167,12 @@ All the model source code are in the directory {{{file(src/Model)}}} delegate and command are in {{{file(src/View)}}} (see section [[View]]). Solvers classes are in {{{file(src/Solver)}}} (see section [[Solver]]). +The following sub section show examples of main {{{pamhyr}}} internal +class for view componants, but this documentation is not exhaustive, +be free to watch existing code for more details and examples. In, +addition some features are not factorise and must be implemented from +scratch (directly with Qt for example). + [fn:qt-mv] The Qt Model/View documentation web page: https://doc.qt.io/qt-5/model-view-programming.html @@ -419,7 +458,7 @@ A abstract class PamhyrModelDict is available and provide some of basic methods for object dictionary in Model. This class is like PamhyrModelList but use a dictionary instead of list. -** TODO View +** View Pamhyr2 use Qt as graphical user interface library with the application "Qt designer" for windows or widget creation (see [[UI file]]) @@ -593,6 +632,8 @@ action. In PamhyrWindow (and PamhyrDialog) the undo stack is automatically create if the option ="undo"= is activate at window creation, this stack is accessible at =self._undo_stack=. +#+NAME: undo-cmd-push +#+CAPTION: Example of UndoCommand push into an undo stack. #+begin_src python :python python3 :results output :noweb yes self._undo_stack.push( AddNodeCommand( @@ -633,18 +674,128 @@ data has changed. ** TODO Solver +#+name: graph-multi-solver +#+header: :results drawer +#+header: :exports results +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-multi-solver", caption="Scheme of multiple solver configured, one Rubarbe solver and two Mage solver with one on local machine and one on a distant machine accessed over ssh", float="t") +#+begin_src dot :file "images/graph-multi-solver.png" :cache no + digraph { + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; + + subgraph cluster00 { + label="User personal computer"; + style=solid; + + subgraph cluster0 { + label="Pamhyr2"; + style=solid; + + // subgraph cluster01 { + // label="Core"; + //model[label="Model", fillcolor=2]; + //view[label="View", fillcolor=1]; + config[label="Configuration", fillcolor=5]; + + // view -> model -> view; + // } + + subgraph cluster02 { + label="Solver"; + style=dashed; + subgraph cluster021 { + label="Solver Classes"; + classSolverM7[label="Mage7", fillcolor=6]; + classSolverM8[label="Mage8", fillcolor=6]; + classSolverR[label="RubarBE", fillcolor=6]; + } + //classSolverX[label="Solver X Binding", fillcolor=6]; + + subgraph cluster022 { + label="Solver Object"; + solverM[label="Mage", fillcolor=6]; + solverM2[label="Mage over ssh", fillcolor=6]; + solverR[label="RubarBE", fillcolor=6]; + //solverX[label="Solver X", fillcolor=6]; + } + + classSolverM8 -> solverM [style=dashed]; + classSolverM8 -> solverM2[style=dashed]; + classSolverR -> solverR[style=dashed]; + //classSolverX -> solverX[style=dashed]; + } + + //config -> solverM[style="dotted"]; + //config -> solverR[style="dotted"]; + //config -> solverX[style="dotted"]; + + //model -> solverM; + //model -> solverM2; + //model -> solverR; + //model -> solverX; + } + + subgraph cluster1 { + label="File System"; + style=dashed; + + mage[label="Mage Binary",shape=note]; + //X[label="Solver X Binary"]; + rubarbe[label="RubarBE Binary",shape=note]; + ssh[label="ssh",shape=note]; + } + } + + //config -> X[style=invis]; + //model -> config[style=invis]; + config -> solverM[label="",constraint=true]; + //config -> solverX[label="",constraint=true]; + config -> solverR[label="",constraint=true]; + config -> solverM2[label="",constraint=true]; + + subgraph cluster2 { + label="Distant server"; + style=solid; + + sshd[label="sshd"]; + + subgraph cluster21 { + label="File System"; + style=dashed; + + mage2[label="Mage Binary",shape=note]; + } + } + + solverM -> mage[label="", color=1]; + mage -> solverM[label="", color=2]; + + //solverX -> X[label="", color=1]; + //X -> solverX[label="", color=2]; + + solverR -> rubarbe[label="", color=1]; + rubarbe -> solverR[label="", color=2]; + + solverM2 -> ssh -> sshd -> mage2[label="", color=1]; + mage2 -> sshd -> ssh -> solverM2[label="", color=2]; + } +#+end_src + + #+name: graph-pipeline #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-pipeline", caption="Pamhyr2 solver execution pipeline architecture scheme", float="t") +#+header: :post attr_wrap(width="10cm", data=*this*, name="graph-pipeline", caption="Pamhyr2 solver execution pipeline architecture scheme", float="t") #+begin_src dot :file "images/graph-pipeline.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; subgraph cluster0 { label="Pamhyr2" - config[label="Config",fillcolor=5]; + config[label="Configuration",fillcolor=5]; model[label="Model",fillcolor=2]; obj[label="Solver",fillcolor=6]; results[label="Results",fillcolor=2]; @@ -658,18 +809,86 @@ data has changed. obj -> model[style="dashed"]; subgraph cluster1{ - label="System"; + label="File System"; + style=dashed; in[label="Solver input files",shape=note]; out[label="Solver output files",shape=note]; bin[label="Solver binary",shape=note]; } - obj -> in[label="Write (1)"]; - obj -> bin[label="Execute (2)"]; - bin -> in[label="Read (2.1)"]; - bin -> out[label="Write (2.2)"]; - obj -> results[label="Create (3)"]; - obj -> out[label="Read (3.1)"]; + obj -> in[label="Write (1)",color=1]; + obj -> bin[label="Execute (2)",color=1]; + bin -> in[label="Read (2.1)",color=1]; + bin -> out[label="Write (2.2)",color=2]; + obj -> results[label="Create (3)",color=2]; + obj -> out[label="Read (3.1)", color=2]; + view -> model[style="dashed"]; + view -> results[style="dashed"]; + } +#+end_src + +#+name: graph-pipeline-generic +#+header: :results drawer +#+header: :exports results +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-pipeline-generic", caption="Pamhyr2 generic solver execution pipeline architecture scheme", float="t") +#+begin_src dot :file "images/graph-pipeline-generic.png" :cache no + digraph { + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; + + subgraph cluster0 { + label="Pamhyr2" + config[label="Configuration",fillcolor=5]; + model[label="Model",fillcolor=2]; + obj[label="Generic solver",fillcolor=6]; + results[label="Results",fillcolor=2]; + view[label="View",fillcolor=1]; + + results -> model[style="dashed"]; + results -> obj[style="dashed"]; + } + + config -> obj[label=""]; + obj -> model[style="dashed"]; + + subgraph cluster1{ + label="File System"; + style=dashed; + + gin[label="Generic input file", shape=note,fillcolor=white]; + ibin[label="Input formater", shape=note]; + in[label="Solver input files",shape=note,fillcolor=white]; + out[label="Solver output files",shape=note,fillcolor=white]; + gout[label="Generic results file",shape=note,fillcolor=white]; + obin[label="Output formater", shape=note]; + bin[label="Solver binary",shape=note]; + } + + gin -> ibin[style=invis]; + ibin -> bin -> obin[style=invis]; + in -> bin[style=invis]; + obin -> gout[style=invis]; + + // Input format + obj -> gin[label="Write (1)",color=1]; + obj -> ibin[label="Execute (2)",color=1,style=dashed]; + ibin -> gin[label="Read (2.1)",color=1]; + ibin -> in[label="Write (2.2)",color=1]; + + // Solve + obj -> bin[label="Execute (3)",color=1,style=dashed]; + bin -> in[label="Read (3.1)",color=1]; + bin -> out[label="Write (3.2)",color=2]; + + // Output format + obj -> obin[label="Execute (4)",color=2,style=dashed]; + obin -> out[label="Read (4.1)",color=2]; + obin -> gout[label="Write (4.2)",color=2]; + + // Read results + obj -> results[label="Create (5)",color=2]; + obj -> gout[label="Read (5.1)", color=2]; view -> model[style="dashed"]; view -> results[style="dashed"]; } From d20cdfc47f81b8d2ad4214fc7a0ac59ff8300264 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 15:55:45 +0200 Subject: [PATCH 022/248] doc: dev: Complete Architecture section. --- doc/dev/documentation.org | 87 ++++++++++++++++++++++++--- doc/dev/images/python-debug-repl.png | Bin 0 -> 61865 bytes 2 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 doc/dev/images/python-debug-repl.png diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index cb40b018..7aa30aca 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -73,7 +73,7 @@ https://doc.qt.io/qt-5/model-view-programming.html (last access [fn:org] The org-mode website: https://orgmode.org/ (last access 2023-09-15) -* TODO Architecture +* Architecture Pamhyr2's architecture is based on Qt Model/View, see Figure [[graph-architecture]]. It is made up of several different components: the @@ -672,7 +672,30 @@ data has changed. # Update function code... #+end_src -** TODO Solver +** Solver + +The Pamhyr2 architecture allow to define multiple solver. A solver is +define by a: + - type + - name + - description, + - path + - command line pattern + - (optional) input formater path + - (optional) input formater command line + - (optional) output formater path + - (optional) output formater command line + +Let see Figure [[graph-multi-solver]], the application can implement +different solver type, this solver type implement the code for export +study to solver input format, and read the solver output to study +results. There exists a generic solver with a generic input and output +format, the type could be use to use a solver not implemented in +Pamhyr2, but this solver must can read/write input and output generic +format or use external script. There is possible to define different +solver with the same type, for example two differents version of the +same solver. Finaly, with input and output formater is possible to +execute a code on distant computer, for example, over ssh. #+name: graph-multi-solver #+header: :results drawer @@ -782,6 +805,16 @@ data has changed. } #+end_src +Let see Figure [[graph-pipeline]] the temporal order of action to run a +solver and get results: + - (1) Write solver input file(s) using the study data + - (2) Run the solver + - (2.1) The solver read the input file(s) + - (2.2) The solver compute results and write it to solver output + file(s) + - (3) Pamhyr2 create a Reuslts + - (3.1) The Pamhyr2 solver class read solver output file(s) and + complete Results with readed data #+name: graph-pipeline #+header: :results drawer @@ -811,8 +844,8 @@ data has changed. subgraph cluster1{ label="File System"; style=dashed; - in[label="Solver input files",shape=note]; - out[label="Solver output files",shape=note]; + in[label="Solver input file(s)",shape=note,fillcolor=white]; + out[label="Solver output file(s)",shape=note,fillcolor=white]; bin[label="Solver binary",shape=note]; } @@ -827,6 +860,10 @@ data has changed. } #+end_src +In case of generic solver (or a solver with input and output formater) +the temporal order of action is prensented in Figure +[[graph-pipeline-generic]]. + #+name: graph-pipeline-generic #+header: :results drawer #+header: :exports results @@ -858,8 +895,8 @@ data has changed. gin[label="Generic input file", shape=note,fillcolor=white]; ibin[label="Input formater", shape=note]; - in[label="Solver input files",shape=note,fillcolor=white]; - out[label="Solver output files",shape=note,fillcolor=white]; + in[label="Solver input file(s)",shape=note,fillcolor=white]; + out[label="Solver output file(s)",shape=note,fillcolor=white]; gout[label="Generic results file",shape=note,fillcolor=white]; obin[label="Output formater", shape=note]; bin[label="Solver binary",shape=note]; @@ -894,8 +931,40 @@ data has changed. } #+end_src -** TODO Unit tests -** TODO The debug mode +To implement a Solver in Pamhyr2, there exists a abstract class +=Solver.AbstractSolver=. A class who herits this class, must implement +different methods: + - =export=: Export the study to solver input file(s) + - =input_param=: Return the solver input parameter(s) as string + - =log_file=: Return the solver log file name as string + - =results=: Read the solver output file(s) and return a + =Model.Results= object. + +** Unit tests + +The unit tests is actually not implemented in Pamhyr2, it is a *work +in progress*. + +** The debug mode + +To activate an deactivate the Pamhyr2 debug mode you can open the +configuration window and type "Ctrl+G" or run Pamhyr2 with command +line: +#+begin_src shell +./Pamhyr2 debug +#+end_src + +This mode add some log and add two action in main window menu: +"About/Debug" open a window with Python Repl in current Python +environement, and "About/Debug SQLite" who open the application +SQLiteBrowser (if installed) on current Study to explore the study +data base file. + +#+NAME: debug-repl +#+ATTR_LATEX: :width 16cm +#+CAPTION: Pamhyr2 debug Python REPL +[[./images/python-debug-repl.png]] + * TODO Build the project The project uses gitlab-ci runners to build packages, but it is possible @@ -942,6 +1011,8 @@ winetricks installed. [fn:nsis] The NSIS web site: https://sourceforge.net/projects/nsis/ ** TODO Setup the CI environment +*** Linux +*** Windows (Wine) * Documentation files This document and the user documentation are org files. This text file diff --git a/doc/dev/images/python-debug-repl.png b/doc/dev/images/python-debug-repl.png new file mode 100644 index 0000000000000000000000000000000000000000..5cc506026c104ac044d71300430c0884c8d35fc2 GIT binary patch literal 61865 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�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 F{{?uG7>fV^ literal 0 HcmV?d00001 From e589fcdd3dcfd3c0cf16b4bad2861451d68fa9d5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 16:42:42 +0200 Subject: [PATCH 023/248] doc: dev: Complete dev documentation. --- doc/dev/documentation.org | 62 +++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 7aa30aca..a68a3c09 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -201,8 +201,8 @@ other components are linked to one of these basic components. bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; - subgraph cluster0 { - style=dashed; + //subgraph cluster0 { + // style=dashed; study[label="Study"]; river[label="River"]; @@ -220,7 +220,7 @@ other components are linked to one of these basic components. geocrosssection[label="Cross-section"]; geopoint[label="Point"]; } - } + //} //subgraph cluster1 { // style=dashed; @@ -294,6 +294,22 @@ other components are linked to one of these basic components. rreach -> georeach; rreach -> rcrosssection; rcrosssection -> geocrosssection; + + // river -> boundaryconditionlist -> boundarycondition -> results[style=invis]; + // river -> lateralcontributionlist -> lateralcontribution -> results[style=invis]; + // river -> initialconditionsdict -> initialconditions -> results[style=invis]; + // initialconditions -> results[style=invis]; + // river -> stricklerslist -> stricklers -> results[style=invis]; + // river -> solverparameterslist -> results[style=invis]; + // river -> sedimentlayerlist -> sedimentlayer -> layer -> results[style=invis]; + + geopoint -> boundaryconditionlist[style=invis]; + geopoint -> lateralcontributionlist[style=invis]; + geopoint -> initialconditionsdict[style=invis]; + geopoint -> initialconditions[style=invis]; + geopoint -> stricklerslist[style=invis]; + geopoint -> solverparameterslist[style=invis]; + geopoint -> sedimentlayerlist[style=invis]; } #+end_src @@ -965,7 +981,7 @@ data base file. #+CAPTION: Pamhyr2 debug Python REPL [[./images/python-debug-repl.png]] -* TODO Build the project +* Build the project The project uses gitlab-ci runners to build packages, but it is possible to build packages manually. @@ -1010,9 +1026,45 @@ winetricks installed. [fn:nsis] The NSIS web site: https://sourceforge.net/projects/nsis/ -** TODO Setup the CI environment +** Setup the CI environment + +Pamhyr2 need a Linux ci-runner and a Windows ci-runner for building +package. The windows ci-runner could run on a Wine environement. + *** Linux + +The Linux ci-runner need some software and dependencies in addtion of +gitlab-ci. + +#+begin_src shell + sudo apt install \ + emacs emacs-goodies-el \ + texlive-full \ + python3.8 python3.8-venv + sudo python3 -m pip install pyinstaller +#+end_src + *** Windows (Wine) + +The ci-runner environment for Wine need at least wine version 8, let +[[https://www.numetopia.fr/comment-installer-wine-sur-ubuntu-ou-linux-mint/][see who to add wine official depot to your linux distribution]]. + +#+begin_src shell +sudo apt install wine-stable winetricks +#+end_src + +In addition, the environment need windows version of: + - [[https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe][Python 3.8.10]] + - Git + - PowerShell + - Gitlab-ci + - [[https://freefr.dl.sourceforge.net/project/nsis/NSIS%203/3.08/nsis-3.08-setup.exe][Nsis]] + +Now, we can install =pyinstaller= on this windows environment: +#+begin_src shell +wine python -m pip install pyinstaller +#+end_src + * Documentation files This document and the user documentation are org files. This text file From dbc256be1a5fafc7fa2e40832613aea35aa928da Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 16:49:00 +0200 Subject: [PATCH 024/248] ui, doc: Update Pamhyr2 debug repl. --- doc/dev/documentation.org | 2 +- doc/dev/images/python-debug-repl.png | Bin 61865 -> 76043 bytes src/View/ui/DebugRepl.ui | 13 ++++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index a68a3c09..8ca3ff05 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -977,7 +977,7 @@ SQLiteBrowser (if installed) on current Study to explore the study data base file. #+NAME: debug-repl -#+ATTR_LATEX: :width 16cm +#+ATTR_LATEX: :width 12cm #+CAPTION: Pamhyr2 debug Python REPL [[./images/python-debug-repl.png]] diff --git a/doc/dev/images/python-debug-repl.png b/doc/dev/images/python-debug-repl.png index 5cc506026c104ac044d71300430c0884c8d35fc2..847f4afc6f94829f2ccc3abc729505e841fa6626 100644 GIT binary patch literal 76043 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 K0_GYX{J#Ly|L=PM literal 61865 zcmb5WbzD|y+ct_i2BLtX(qPew2#8AAC?Q>vN;lFC1`3FXiim)OA|N0Q5;vhzN+TlD z(k0!!kM+#V^M3FCe*2F-e>gMD&AQjR)_I-hQP=p%%U<2RlVK+r8QE^hYnK(t$hMS{ zk!{-8u?;_Y_GDig{`QyMMM>oy_{)8VfiM1@-u{ZZy^^(&z0)mQLo#D4YfHo9cKWu4 zhE{ea*7g%yip9yu4w6Y;zM$;<`cH?8^FEdQ#VPAlNsi%nnNJ^BJdN<~IByWZs+m{EWhzOPq=nftG$E;{`qBJ0VAPs8{vpE5wcpmAu7qL8yzNsyub47j7$6)UD!(L>FG@^EiI!= z1;b{aKBX39*zHK_$fRz!TF6WO_ZmBF9=5i&-af;0MI(7W|B?^o*o=G1(NZohu29){ zfx51)E(1Y^^tQ2I79VGnH2!nT_~R|@Std#gA7F{qH9{R4VsSHR(!fvrp^TeBDDzX0y%pWPfelDwf<}l0r^S{@EP}8h4tx0Up-N&_R+c)cLuVfY!^of`Dc116R}NH^F<~HADDmAX zS^k@L^Lc5_Eh@XOyilEZTac1=waV1gG<|txg~ODYvcf)Dbu_-oEM5JWaps?Y7XMHw z7X_uSfx)uOTbXYWp0rHVl2Tm?%BS|F=iTNScxe_@rIggPE)+h-9`Mb;_4`e}qg%eK zX#D$Ffl@EHLV0{TrLUbg$Z6cR?9*^gI9$Vy{D8SfNMl}y)oa;!iQVtavNa?h;?aB~ z6^F@p{)n1TB!2&#%jZqc9<$nWxF)};m>J66R!;Rguz|XvUyvbSTTWZV+ih>NG>-Fz zB+)+NL7A+Unx5#Uo$#nG9%p7Njp;t{%)|PeaadOorJ4>Hqe>Pbo zSt3?(u4yx_6VNs`9=w@?_(G{L11pz#Nh^BlhsEU&I$AGVwp?(uUVKD#PN(kKPnO8- zD#Ej#Bbl12RM&TAu4mlQJ zb=#^9{p?f0n!4^UTevovDD(bFHcFJH_?6Y;(YY}~c0FuJUp04G<%F+^B!==G*&m=dR39K)|@?b4cCS@bLm89V-l^>}Zl`3CEW&U3*lRyV3T zQpd_D-a$ zy^hM|NiIEwKG)ROHOE&gbeLLqt&e68hN`aZoblW^d&;W5?6KawS4X8v$zJlNUnXYn z-wIAxMlKDqGYFq$BaKj~uQaK>Al)h(;r`OKvAe{F>S*76MbSVCn^S*`$0~!FHJ!6J zavo0Z!7c)e1pJ@42jqOrOKl;b-yIao3oLzFSp6$F-GH&$c@w~yW zpo301k7*Jv?A1e&(eiAsQ1z%@-nPFX#31PXrmVHlsb8$2BBizEIl6tpCz7retlwgZ z2z@+Idc(4!Wwra9OMhXFx`5U1?yE*m-wo@X;+SSFUFBfye|c9(sCebv>YUelji!v-wdpUP2YVS>*L8gZWp7%U3)Be7 z53kibiS3idMmdwZ%ky)DXOWZn)+F&WU-vgEzT!I)tisClN05Hoj>S6CBhq2*IBZd^ zb|!}(KS=VS_sA9R3VCb}YP4r^AKkj1w@Sy`q_euc=$p1aFI)H8VahGP zr|RPH!z%@|^RgBkvOz*V8#hD;&w1s&U;nevJ~eH{w6SzKmf~oZ+|k77B7^G%0aI_R z5@#Hh*iVEK8;Itoc5kYE=z)tnY-}1OsQ1T}chui$**!8T?wxSza@UmH%dV*?(;sHa z4sX2emyaFi*k8e8lgLjlkPyAM{f<<}(*2wto9-?qy%rRFH9XwX>|k)Vt}x@-Z*$LY zV)`okJaQftas)Uz2B-Xf;5DEyvF)~g68AuU8a5~-yF+-!CPgs)5$WXXOicg@%r5=FHhRhh1CZIG~1X+VSyPO z=ZdT1FO#IyroV@~C}#^QdSrQLxN5p}^huq^O$RC)>MQtopE18|AFuyVO#hLb?4zU+ zfA^G28oop=DF1k~@l^7|EbhO&o8-Wsm1b!~48YQAT^IPlfLD$hu~k+~}^ zzVeaKJF1^!6Lx*xjj_B|f6;KfsxJ>TYIR7@8utvf4$S?e{grAZ`7z_k@a@Li(_;=j z>N~$*Ij3$=c%E%zv6+vBWXV#;8@4_7cHWQNk^B=RlFbq8!otvJ#rcm#jFS!8cu2vb z8kUteSKmo)EW9w)QXXG^$s9Q+6p~>+^OKjx$U*&pn$F;-#>qccB46TCofM9T?7+I$ z27OPAcvq^GxSFh(K*uNg95p}dI`+LYdH;Lmr{Y;uq|>U-{W(_?otCwH+G+n&Qnxit zKbV#Z-DH~aJSpKqHI;wGfAM!?zzY|C>6J|fHl}R;IMo<_pL4Qxx#K^OSmLg^%+LsnJpC~c5@wzBQbnsd(MbNx(Co0QXgybgR{`0S!}YE-;D$wIK{W!l2| zGOy>&syjE z#*&NCwW)Mh-ZXCOz?}I-%Kkn|N_=>jdz1i_uoeg~sqXedq4d*>BT&-6auX`NfR_{H;2z36B%S-?daIw7h8#AD zg;f|?-x`ZN?ZIBclDNLMLb`wN-tD|TL1Ky!P@$)S<4iQwkw)XahEh{!wGGz}3DNS7 zDe$*{|NcFadi%C*QatH@fAXR)YxYo@;NIn3Y~Np8j*cQH`wt!}V@hak7<<)C!}Xso zM_d!1Bt6RCqWpt-D7!!5LOC@75r}WgU3_2XwCSI3zUVvicVGX{ebBN=5*y<`zpnM- z=pmHLe}2hE;rr$@OiyOCDgU_!b9nwA;8awf-e}8oGHO;pBch^=Hq(e;q{TVIC z|JMbdd;b^VU6K6HWcm9JVrRmrN9~30-Md$7yeB`lc~l{(zv%8i_q6)sb+gO~cJ_yT zeFpv30TLy5Hf^C3aXi*r9&kiZaQ5*_uQPC4TwJ zm6P^E^)p(B|9Pr2#rfBLFI~K7Vru$%*#2$f@Z`e6xtPmMA<@wX&CJb#NIx58cH0Zf zx!vFnPNbD~?4}?mf0LfR=g#lnH~YK(UDNdMj&oO{#69TvZr*vW{)F|M&eFoKbp3Bt zRWnlR0TM@Woxiqi18(Nxuo6XPqabr#qrA+~;EmFv->c@8CRffO%{3vaRx#|}f$ic_=6lLc0N}P@DL3z1^<5=qh-1hVS($dRcN=jO? z&1uQ*Z>NgO&JOOa2z1Kt{Qg~0N=l0CcAi#;>On!l7q4E?IsW-|K~pol*;&`8o+X;)WaiI`_`aqPRcZSnN<{L$3pa{N+K zlb#?$OM#03TFKPdszjAGk@P_!~em+`vJ!u?1dh+D6Oao7V_LM$b?tjKYmYZek<+LdzD3_L( zD=I49E7SX|moBfMaKYKxIb_t1)RC)|!opTUCL2$atr76~=BMu4oQ89g$C5b6ZS3s@ zgM)*WRa9Ec7OyhOw+_`hg+$w<@ zo&R*1p7;+{2lS}!e~v!-8O>vPd094I=9)H~HvZsLs}wi;%QSJa*(5^0`m%d(oZVsbH#d0JT?KlTg`Za;tJ z%2wQJMfYt~&?DLSnB?T$q>d*g-Jh_vBV?ORn|rZGHgDN-)_zDnWdEf7Su0CR6^(1z z8plW>|(Mi2I9p_m$pnzP2tk6*`GEuo%4MC`Tq9CU%#$tYey(h8;3>BZ66UAG2RWra#YGr3<=bqrOva%ZZbaxBcK3dwwRNZqZ zl={)RUlv01V{Ja@IdO3hhlWh>j)bqXZ`rbioRV_>#$B{(Y`x%j%P3W;P%2h&84V2v z?%+pg7)n}N)p!C)Ik~~-(!85mH9v|`g(Rk?8g~_W9A#&J9v1efs{f0HkXyF5nyTva z*RN$ZgTC?PjX&eZ_m?U$^ll2!fN#^S8IiY{Nh8Wa_EqWAW{n`N^BAKkBF zFRulvrsb16cJF56;Yn(oK>NF*q||uh#HC`-wV}406GoY#Vs7)-`2R&0w7C>-AE+m* zx)p{EU3Mg`W#u#d7Ym%xmOoy~8zS8qPG}Is4ZT&1Xo-39FADIb_;FU!H$Xx^NABkQ zm1I>K28LFDw!QzIVIrgCHaBk`lsmU3c<9if_`=8S?OFph;mvLj{`;ARQH6`5Ha(Yh zT!+~XA3iMWvXwh{0ob zKut$?&Bo^BM9~VbgM$MZW&oeZvOP)#qS6PgyyrFHrx!*Z{`+iGjo&RVilR{qS+#$C z!YuMi+(Y=t)h7mHt=VLyrKR{z^|YIt%(BB?zC5svilsWsv{Ajl`Q+6nOos#nf-qtY zU4l5|*m-#Ti`T}i-#3|!=MAzXCMJHZsyc$%mS;DZVeoA)C5tHL`;MBL8ZwNKndQ;c zUPn^sSvz(CFI)Y1ub|ETdTwqG2+v_zTT_z} zYGlF6IO$JkA*o~`ARu6NxG}!%BB{eFbq-=5i^r^jhK5FdgxbH=NybV(h0ZfIGt+y* zYl-53|CJ@t)2BmD>X%P;`>;9+cP-D2F5$~$QBuH|qm%31*TaQ~%ENWur=*md2rBC8 z2A1jF0(Ps8Iv0iJN6B>NJa!a7k2ufm)kPch_g7+WLb~o#TXB2dK0Zpy%J$rC7yh}2 z(kpRQ-p+krpZ#giQyQ-Qla5vAEnc-nxwQ|wKX~xqm`0YbWDxsFlra88mR z<0v51Pyu5~Y+FGgp$wCT!^aL#xXu5$_3b&EmP38$hKQ zF3%2&PJY?~nHs3E=F0;LAcOdPv42-t>!>mT%tq&2_4M>=oo@X5bKlI`n|x_r8=hTq zgsSm3zVl4{KMBVF4?f`Ky0E+UN})3Me&Gm8lZ|8*?xq{sQU45y*NKY;I{8TTsB{Q({x(Z(F;#vO>ekDu-eq^Pys87Z+zacdnRs@NCFR z*gtn~A5QJ#?Op%<`@P2D%khof6BAw%eu*b8T)6P#=TF?%y&9vEVf)DW3{RO3J4+!( zHa520j`11o7k2EtyidZy2nfCjyyVTtk1um`bN|=1O-Rdc<)XxQ(ea&^JXR6Nn!4)t z=T~}Pf4>Xc2LRlxEI|x>SPhuX@@i^68p&~B&e$58uY;5Acy#ZK1bm*?P_g`da zc-J&FHFJWm$;z?|3o`^rTmlRrIZarlzE-U=4AI_pUQ&{rXmr>Q*w10%;hX4$Ea*_y zFM@+NjV1rU!UNSAG7u>Ec~DSG+O7N3EwLdwmE9#i2_HTvi_gr==zqGi2^{EcgUK1! zSx$ln7#q*5Ep!D%Mn2I~P&#_ivqHo?4zuM%@# zOl$r)lN~`y42ju{SLanWpQ@s@;+Sb59cwuCK;Z{7<3=C2-GP^_^|3oJz*O+U4DCw5p z`ztc5R^syIr+~G$ZXLoLPfO#wc=00H`{ZOcF)`+thHFYnAlaJ$qtUAiUcPA@<_ndv zXvw%TeAq)T)XdE6g-v$kv}Up@_VSxIZ{EelWjIaf;9rJ^hu^(>mj+ZJ8{&3Yx2;93 zkPM_nK0(={tB5(iQPRkWebc5*QcL;VN@{I7@r}3m=z#F1xno)^*bg2gi;xABQWHPd zd|F#ID4B)5u|boXo|-a6rP|K8wFlD)wU1+?oJbKos$&Ks;d+RrNKh zh32=;M~>HWba_v%b~G)HM~<8S)1Mj`^O-MwP)+()Tf425CKZbU94!2J)WY06COVp| zP)ANx_2u{EpWU~KJ{-G$*5dt#5APr(VEf^dCRSGE{k9IyW6#^h%7^Vi5}I!@9lryL zlE2d_4iNg?yQ9z)PKL^Wc15bX@NjcqyM8?uq6VoWk^6>cTtw$8@A9~y@XH2lO;BZTw%7nT3d1$PWhNlA&roo2RFw`QBS zWEfJ&#s`52rvu+ol1l*q2}j79TFxFlheDd2=wc1}<_*k^)>!ET2JGdv!P`~G2!1Uk z^{8&A^UH(=lNSdR0H&MF0FWButCLz`l$mbbCH}`z z`k9~w+{kdrHDkUWTuXG`k%(?JTsbdh+%aMWKmJkf-0+l_>5HYzqj9IYx%i}uCC_=JDD?bPT3eorLC1Vm zV>JJ#WhO}S;il)$pM!US2cfA;DJV41TPyjOT|~3`_U#+yCP0Ubic0X&lE1I-%NhXY z^=qa+zV_GHJWKIg$=90je$l-F|4=jN{MXC3{KLg+Nfe0>e)ZdOQKjvrrHpS($l4)Y zMTp5*m7a>Ys(6`yNgMS!LvflO;M71`UH50SI?i{0{rY965+_R^P|hM&cJ|n@$G*P4 zcJhb2rRC+Rq0AT;Fx~%L&r`Lh=i>K9XIg6N^IBT%XE~ou#^&@y$i^$_#B!ASDRT!; zvj;|Dk~emr3efiR_gB)1;i{#A>}X0906S#es*dMyrYZ}ooLgzB%N+lH!pNJb)D5MK0}ga>kOb5cxc)f7xB{->~JTG z`{d`wu$ObN24Jg%RAk>%; zX$K7#Ptx%l?4S~xtzY6TzhToGaN2flu6eob^({*{+j0Ieh~I{MMZX z0y?soVRGV2N6j1t6Rufn1?iehQQrLGO)fh3LrHXFbw(yqbgAG)x84tPeMBNoohVv7 z%<8##sLN}ku-S<{$ghIM?eRjFR}|WWlFmn|*pXoV_34n}qg-6%=rfq-7?2=V7bWL& zth;1^qEbI32>_2>WpVL_N&(CcBJ!st(Rgqcu_AJ48@7y7C9B&7dmriRN6Ay8#6I-xfR-V&c8&ShnB3M+>>a_Iq z%uIvX#soFx&YeBuq=~q9@4Ttbt;#Qno;@4NXq9`n$YVuOMWqZ^;pF0)y0I-nwqmYX zw=V5gN&T?BMQfHHc)zj>XDejuyw&L{^4+`pL5B(!I^D2mqSy8t;zizTo<49~qZAwO znxtfR{%Fb(US3MjG}O-GP)uL}%SR*r(A<&4B#^?l2?>6KQC^+qH?UiMSX_7bm%+(i zUl3^h89#c_*Qr_CiCIm{W-F%tz81HL>y|>d)Aj357Hd0gIyLkCMI1>H1qGt` zk9EVu0_~Y#zaDlOlGw)T;JLcWf=g4=-!DK|l$n z#Var>h~b3rjy*!OmNEDE(N^=+{_oN!`u#W}hbF^iqr@Yst7TBTewiQF$6u#9+r6FH zZ%5Lc2vl(z%oiwR2b1njA&vJq6|Z#M2wkv$@)?-hbEU1zanZdgQN?|}#TXrz-}L)Y z9nU4h%Brfuj|zCga~n%_H!=+Tnyp7iUH>SD%CH+%zq$bZ8oD(6f|ti`yj`WdziqeP zbl*1&jK=!p@E zAgT2Aaqva=>2Dksp#3OMi2#PO`&8mz?`@@kBpdHA<;sl=62{Omt=y;i`NB|Ga_3vk zms;vP%V+A&1-4iil|9-&=w3*h*!fF`gZ)LJm^EN? zM*E<PTzyKKNbergx^I8t7qnW|SB(GoZFQe5h?YFh5V0PlW_2nU@vk4C9mEzC zvhJ)NO)Zwy*4FmPsEH5>0R%gCpo-plw9(;EDG6&uY*K6`G$IZ$v4}S7;t0rB5%=zG zh9Z(*c+!^Lb+1#=@<`H5Mkhr>8Qny5q5q$rGXFxIt*^quDzPkA4GfN=XmzScYx8Xh zsi}jgr-S_b&q)(S;oyEir9TYm-OEVhdPAY5B8572cr({LJj9581a)7`^UhFpr~m|z zW9n&l4qbY98~bVMmwsSG_IC9!8Fm?VnhQU|_1JF<&W=&`1CVK8bo zF)7L8Wn;+r$n6xp4$~;+IajfkmKIb2N-&C=i3z&;TYy-ojz#Os=FuK9){c%sK9tPU z`R5-81`fuBTOIzJ7!&O-RfHTmixTf-Rb^$tPv*UQzd+!D&Re0Eo@U!$N$JFy4~GK; zkLW`fPOsASE1>UIqgCaOrjX!d8J9fVExNwgE4tJd1hZt*n6@iWgA2xpd&pqwsG5m*IK5@u>ncRdXW8MG_J zn$C_k2Zmea`$4rwD{zw*^N=(D;np4eudZjz;g_+?X_QwQ@Pq#M2%SKeLF3rGV zviOyD7dw;*%u!40bw(k=iFIATMFQv<(C++;i_ZbD{)T3x;@QS%U9iW*dQMD1U7fCY zwf{u$SeEOYS?Ylcx-(B$#KN!x*y@uZu0n0gbYHSHs(md9pm(Bhb}!sDk@4IuJ|&+; zCJOG?7%9C3_Pqe99&hJNy=VKTN^(!UQextO*H6}pOrY3IQu$%^L^qi&VWyG&$}*)< z({W)V`d-z`}v?F#9FE(sdA-0p4mTvNT5=j%&k&iLez%; zR~aAW*&5Sa(=z4=4aL;TiXdY!#rRF?4?^$G#n`16w&x@e6R{?`UTfKVdA99lDE)9F z(;0#}><)3BsT5+cM!G#Y-RZm~=vygzNd?|$Vl~Qe?MVRR=?su8oj|7|y|R|aPGs0& zGOy1(tFM5B78$^lqxhK`m?r2yF3<{?FOoEK`w$wI+LXP=FQxGNYLrlV0o{}$z6?ai}LQo@Z;>lgF-GDpN9a-VBb17`a+%H#sbI|F#lHRu6d09gFR-1?~QMvo7x=O^g2x4SQTqB%JS z7n4TQQ)+K$H?H>L8xO$Z!H|ytTf%L;#AItAYF|x47%!DRtK+iu@@ozU9xd_axPBMI>@9HKH5x8-tR>Wm>s)A5k z_wP@~f@5O55;WbL)8WV%C^k<=HZxcUcb2m&%K2(dqhUOx=zr`1DQW4#N&@-u=Jo9b zu)ZuUO#vUs0C!LTpj9bYR#sL3cz`JcpG2CF7+yXf_Y5H{0JQT?*U{tXb>zEtDG#jm z1he8Lu8tT?xR0gH##to%HfcYFGjH5xFkRM{098;c!}umdKZ+~UN>x^;N~<5heX z7QN`&Y$Nl_lP=Q+KR?`Pe3LYYjceX+m1FyTdA3n*lkm|}pbpso#H|t`jsHXrs99Jj zvz#cxM4Wh~UmhdG1DI@t3g+T+mgv!=Ka-&s&bo>rf`L*4xTP0!6@>nbsmOEuC<+jQ zoW39fltY;a;b8bt;QnI{kD=O5`J7wbWYTq2lr}LvJ-xDQ5M)ITdr`nRPEH5+b3a5+fMG5(Zjp_euxJDd^d=H^&5y9J!B1aDi*3<%ttvt^fnV z7?ju1k;RO_O-kjt3UoTq^3)m?NJ~rW<3Cqgi^0_&FW&Hsbf1>&r#l&5gRrG&~AuPK)8avf#-}yek2rNIxx@QPwDAE zgZlz4I+!AIH=__4b8p1o#yE^o&gLy<*~^c6h!$(#8zB`7jdMGA7YMN$w9;@POTGTe zV35W**$62KiESr}SNRE_Hnl*Jexnt#8ia%9bS(`HDU=KupjQtZqrQiXRyQG^z+IR# z$wfBBX!7@GQ0l$R%r&T=VDAzW!3@(z_8(@a2yn~x-1S!NwAOq5JYe)hN1Bk6)x{^mHidEcQUDB7Cayx*HVbx0qPLcYtId!$c zW4d$O2&)+A5Efv6{-6O4c0elRDrEVfY97+>ow6XH>wqAPG22gvxC-)gi5Q|MqV*vm zPzQyhaGU|#6b{iv7zf$e*;=l{u~7CSo*lV5-ELQh0GoX5W+cMZ#{Yto319bHb+tkD ztCPoWWO(B<8HQB{`ekPb3maXnzB7VILS1F`*atNh`-AXuZrr#bCgqEUCxQ0+5Xvhk zv&YK3gR4z}c{A@eN~W)nZON&qI6z1W-4^Ce)*=rK(U6=YhQOhAqX(Lft@QYx5PnR- z!^!yx%m8UEzzaG-lc&$=P#VN7pn|t&1&ZZj9Sa8_{HyY^h{{ckniZn)ms<#B0yRdj z$^OEOzK$V_)}*7q2d#&Q^^xz`@ySFG;fzZCK(U|0X96L*GcYjVqznf>hlO&=JD2H%6f~8_#^PzMX!M_OQ1rQW!TZ#hjVzfu`VpFQ; z6*Dt#yTRI6c!9t+dl?!1!9$XBI^W_41cF3nfY8ryiCk3j;`+*jNFl|Txh5+jJU}i4 z!-CCO5^%cH7ykcLPnou(?r4oOEi?0VXzaGHl~q-z(P8bNxDl>%)RZhV1N5#=uMH2j zB7){!xW8?V^AHP~5+)0Awh5ZNrAG$zRT~IW*hZ*mNb66r6(Ta7jnmaRxw-Xl)(EX$ z0HQ8tovDe*Z>+|6lQS(V>qMrku}EkC;9#a@n^5%H!&c-%=+E4ao^FBU8~keK^LExr zNJ@^*Z$}@{P2g3P9|;uwvV}_YypE2JkA9t6!RWDs;mUC9qHmAs1)P_MR!SsxxO@a(z1k;?*gRDA)kvi;lXe^lkOL5YcVcN3cyhGBZDEfpvXyJIwx9r_2wa*7{K_lgr}5!eLNs(vV$A z?KUs6&yHG3ENSs|&>ptA_(3sYs4<=j0mdIce%R+OV&BT==cQ<5KLQks1b9LOE+H}T zi(WdXT*WR5ikP^#@}zsPej#{m9-|f1b_kpjb(t&WRRP+p=rDzb^BLMA9kw>mLl0WR z9Jf!!cn2x;jLl&@E z1;z2h54^pfhK7EF7o8}Rs+vR^%j{}$Mcx-m-=E2w)N&NfsJl?UTbqelvo%mP39+Wl zq>j!Q9~Qw=r(CiX&%1^2#WcLvviQ8g{0@L6LVREJ$B!QyD`r=gwMBc+%Egl`w6vH& z#*P5j;AkJ5DK}BdI=yt~!bfE@E6(FOsDK7wqVwnPs5zYs1bm$z?{F)p>i$|+r&iwc z*Y@o*Ka}LSIDSHj0W}%fH8WpN1-d;|VbQz@LKRExeV3T{2tM^Ervl;tPr*W-V3o(z8jm|>1B``=3 z8bb`Mg2f*H2$obJOMLUTU3Ae*Nrqy?L$e-mwF8R$6s!}Q5QJm~Nb^0{%>Yx|3kesZ2mfUzcnZ;axn15IVwRM%4 zqI9pQ7w~hc@BE^l({>hb_uT5_7K~M-d&2MBxg%@6zDbJbWBhjdEbECNO^K1wQ6zCu z4^DZ3E|{f|8P6*y?18t&>`LaWr-QU7U=PMRvK=O|3m1XAx7~l!IOg8tX_c)3X;QoM z{3u|F$RJOr^XvcY(4vuds!5?I^|#?$p)x}wv9H|=bW|73$n*_xvxyi@LXudGsArmeX;77y3 zU4uiD*U347(!jb)lkS1XE6A?@lcv&mf^uuOIvps!JY>52J<_AG@Ll)wsPdt$6Y-=X z;3vvP*hh~+rl96ZI68jhuL(_IZ6%eS5)_<-bUU@uwGoMDc5GG51~wq1Y$Rupf~yq7 zw=fCY551szm-16nSGNw&>WqFSQmxNN_v*AYi~oV+dbnrA4QArct&tPSGw4|CK?!aC zrM2U6uH^*~{;noyLQg#(Lc1BwOh-RWG=D`K*@D8Kc6#*g+LJYrV&OO|JB23v+cxh- zinUm1&fB;5AjJ1i0Oub&;BR|~53i#?WfyC|#-&Z(+uM5sRaoD7${jH-14F}fAWj{Rxh7fCSfmN` zKVnPcY=9kx9-*b+vv7%}L}yA1Fbet1e;z{;@~^c2sw9R&rqBwIFHyju!0tDPb@5lq%V)n42}W)uL`u24yhFWA6r^` zwP6~=aL`WaRgQ2M``L81O07_Cpd~VLWBb;v1UgF7E8F=qRky1*UsExi*<4ByX=101gER{r%3T!bVxQXYzGk zBcTrX4J%*pQZ2$`BaAi3J0ecwe;StMh?64RRNBHffN>!s_n;!@Tr1-$BE{V9%&Rd# z<-E{x=(J^PDbiE);_heQjY5H8*_xd4lZ0DF!G?;u@MET8{kfcmWSNdQO^dXEl9Yp> z5G+(BT&&K=h&kcmEX<`rP)$y;B%Wl5|K3nYv7 zTq?G9k$%|Oxh2vVv+!NFJX4B)DxlOoRLM{t%~km0(I&hDatjuZSOL_ehVWsyJ(d!&B4r6J+8Wt9FRqL2NaM z@3MxR4XHr@n?BZJBdTzQ(tr8a`iA+i;jk}f^n1>0eryL1c&mT*y|eScVFk%!U6*TA zCJIf%E(e&Z>9xj`o9O-7dn)YL4#z9$PLCzX-}+uV;s4^n+S=#L8M_C^51QFd6h(2! z88{6J*bNBGL<-o|zMc_WO8M~NF$FCzAGSh@ZZU8BOj!7Y;I~=9#acwc;VH8qSoZ9a z#5^%C>AfKub&eIwE4!vOQ}mA}%9rAUZw$B;i76 zezdrS`0iH&{@~TOHQ$`d%ggn|7=bwPLmI}nanlj(l$P9G-}-H9BSfyEnj9S+?f?AR z1BdID4Ro?w$VEB3xZHwU&_f^lng<@nueLTKYHDF=`M})V+4+pqL}x$Tx|`5Lxp(%! zn{ggZ&SCf?5-KBMYs=5-KDi0k!N~qSFu;CHP%5FMaATA`Jf6LnX zM9N97H+>>ow%m8kFy~3Kq};Z3>!Dz&rKQ0rXr?7R@QzL8q+(FGh&RS=D?$ zn&aqWy6q{p^LnpL&@Q=0qYM{= zVq|Fd?MnmUo612R=+SeR=|0JZoAPW`|L_#ViX;&ZgQ7zyczP!b9LE^Yehy=sz?to{ zJ)2|IvDdQ?*3S{p5?D<7W@b_l|B*k%_P4`vxSpOKZ%tLa%k=E*WZJy_2O-)auY@_o zP@U|IuLCSoR%|y(+EB={>Ieg1P8zE~^WTdI7bde*sqFiF`F5<>Hz1f@?yAQT(>)9bT#IBqw9Ypb3uTpf;&LIUm( znkV9okY&7+9)8Tgg!rIa9EtY(sNl6!Qpw*?JN33X94`){x51Z$9=^@k*cjuwN#hB_ zeRS8~zkeURw^dAru2OmTmhF^;$^_*<-F{fn!otG2KUnSSw{P8$eSzSv<<`TBA$YsI zy82fHToz%CDDQLB)z!^+7=4Im#%m?wt-0FSri~KE_TfMjgnD_-NPy;Jnjary==%?z z04?XBmAdY+#z`x-i|wp=xJY1$6B&e{Gta)>YyCpS<93`;4%dB{6rB0W%C=L46#iuE-zGWsGmAvvvmN#PY0AUNKE z%9%dN$q3=lNWm^h8Ub^o&HmTxfmj!zSd?~GMu<2Kb&zs)e6(MhHas~Vc7K9br{G!$ zuQs8v;fw^#7|Aj=MVc;Co+MLB|iaoVoT$hnC0Bov` z5K;T8|{>@|HWs}zTEq{?mAS6v#MDt(tKZMZ|KMz?NuU8I(8JEIGO2Dq5; z6kdggD`{wOFQ&bFcR5*nvxC(z5pu$Jxed7M8yU@4pUpWWs*%bbiWoO$wElQ|9vuV2 zVfc(B`O~@#TdteyLfc2WiHOnB(bL;Zbc#WoVPF0Z5v7#9;plza>RqhjuR;EiU|vKM zK~!V^4KcVhXKed?E5&*t;6p8=2L(9vwv1Z!}tV%gXZ0zib!ELp*PaqNX{{DR)8Eo_&Zd*5J=lZzojKStyFvC*@ zyB9eH0|YFE`-A%OeGZv6r$l@yEq(VnXdJOKoB2OC_6roVxUTNMX=*BoE;;lwnGr^l z^Us=wUhADyR8-}GtaU3B#T&m4t<2T;N?xdQauuY-?S4k6h+%)M4wYCWyNrxXT6+4e zf$C7Aq6gg)U5BRl?E8i7?Cd-@)||!;^q@v@K(yrT6@PomzPCOsa(iL+5?Kvs!Zs6E zsbH0(>0!{$h2Iie�e?V5#wgQ{+!}3)fP+Y+}NRbdFZOeZqyReB|@)mD@&0P^9CZ z-XKi%KYy&|TrfRkWiNjmF}O=a7Vf`)ELwlkwY_xg*)@jhQ*G2*+S)4jnun$=qPbGF z^rJ`JnlA+wi+uR-!TwRL$*o&&gdQcZ$WbXKWGU=XZTj5aUT`B48i*Gy-u|OcTCLH| z!;xaUcI`^b%rt*#JnWFa!Gf-p&D9^aS=%8x{y}lyLv{LZinP^{VkF7&PE1vJT)Iau zcpV^`qkvJhv(emXTHFMoVC?AcRK|&hGywDLI@2fX{Cs>Swj!+Ye|ib<;=b7Je5_pejJTAD~D@2R*&s`0dZDty6>xhm!r5YOZt(Z z#6cLD#!()sC)(Z77Mx+wUJ(v6-o(Sh)9EtE{7#X}Ht#iRomaOAYvjg><5AG6U%c1{ zWgt?W5o3v1DE#k}&|jZHI)T`n=R9>-K1A_a$P4~}2N3CDz0&3>QtaG$#s;bpCDWqx zb}H91+f6Pxw-ZbF8zz@=!`mynT_eun zKsCR@9c;TWuKm%XESN=dzqO6FyxN1vDx&Lk_Qh$ z4vCWqy$J6KKulq&_Sdghh|olOt-HK_{n{B0F_P>Swi`nG5qu`p4Q#ImVD1U-#^&Z# z7*&8Imx4C`Fku|c?;&zt5Ja!Ok(Y;Kg-gQUJ+O(oE>cVy-7fP-6Lr><1=L>ThLgilQoPF7`K4nO z32PM@8>rxfl8ktQcK;GG131r3@O!^g)g>kOeSLReOC-F1{~M2n6rRev(1!lJ8b2ud z-!qNnV1w&Uct~8m+B-H@gRD-J`daH`@rOp!q=nR-mZR;xDRL|#-wVr1 zCX2?n-AKO{k9Z&ZrY2 z=mspPc{BrR8k+kL9&9ZJqRMPu$f{>rn>#a3-&zSYI<-1eHv?r|&(!oA5@yJjewT{I z4B4|Gbn28LHu2&3T;rRA=0D^tB~ilQN-ChI$8ySnCmclpd;U*7a}s` z6YakHqY%AuA_w2l?n{c;onuK45 zm+&@|@psU@fPmd?A1!;)Hm=1gy3UQ10qv>h-adr*u$*Yv2cgL0TwJ#RO5_SVi+RFg zidj14g`8SoH0)tytb#6eO;zFIre7diYrmIB|T2OI!pt9NMV8VC-+P>L~I6%Ogck!3r5ns)p4?d)=qGDb#mXGDL@ zwsUMk#eojFbU&cQnX&^>BErHr!SK$GvYq3>hGB%PW}%mw==idMiOH|N5<~4uAw$Z} zJUbt+jg_vRiJ!@uGYHudllphds68CN*Jo_*;aKWTfPKJU5a`3_rh7+j#7G3Z1y&>S z)S5X~!v!{8t;^b%>RtMAe4QQV_~6>ezCLp|ICwwsN_j^|1l08@(8tXV99z&Hu_hmq zldrfSQ3rURo|$HnAx-`aX*MZ42(xd z2U-sXeM6Y~4I(?ZkCV`*Fi$y;AMb}+rVpe2S8MCuJ$pVQ(#caoTmq$LN4VR z#=EMjDqOCqc@#FD|K+z zE{c2PvujyNY)3)V|CvL>K{!za?%i8da=8CrrSBX(4vw7-{RRc&pa*!}|4eY5sxbx#hNPKOHG@gAHrD^p-g`t&>oVQ4k(* zckf2&KSWAu1_C~GdL2jH4A%Zq7<-D5bnv+`im4}0o+OnVfjG&R8cu)=!YPE(@lRXwG+CM8F5+jl8Fr*$P@ zAX2leLU$Z1E-D&=Bo0^rX@-DNy;qbFuDW;nvwTVifCFIKhHu}rWWA!cX8O4pIy#EX zeDB^lPTRiUKenQ9r{3%Q6@b>|D9Tv21!Zg7sq&LbseP%kAm~8 zzk2mkGEw#Fc{vR?31i6j{t-?QJ?I9a2qyux@-^rjGoB20T?!GfM(0j+^k(9}l8Ooq zMmmq8lkT?nPRpiST&n10dsP3G3@4W_N{SY9zIkVFSR{22 zGAemWun_&WSJMH&DG$)z0uCWQBEOll(tXM>N4tsny{Pxc^y@~vO3{v7yafqRf`{!lNkh?K4Rs%}6g%7?Ikv&skRe;s#SVU`+CQn8Yx0P(z zZ=p4j+{X=yl1aUzwU1BX>0$)vdJ{|1BchutLGvw?#N@j?*d2ye_#Kgvw9#h_tol#1<9o)Kk z^L)@;T0!vGJE%c;3lsse$X*aa2Ts~qmE+FiA+J24q3I(Q7S4@VFt&+#0F0>O?~#vgZvp~SFX)at`N+EMGVmPjg6f}mf|{19NHm>XHisK zY-?}-0R*b)!K%EVF}j>Vd6R_08iDK4w6(Of)T77PxPJXZf;iQPsABJJ%k571=IKnE zIyHbsL31@WG&crrSArqF{C(d3SA*4FO6J?~~9f-Ye|M1@!Pab02Du~VlO z%KLboK0Tty(%T53J9U<&-kG>vjlts?cufbRdQK(lC|7uA^Ahj*Y9Jl-F^pM0rY93F z7ZsWQA*&)yfvcC>YX0(8>q?*G=lh5Xr0D)GFG6+Y zo;&8&)CAQ}1%}1UkpT8qmFy zpPwHo;f$~EtutP9o-_%fslHmf@RsGNyzphR%RZR&cN$+jpt!gg53UM=zQ$jA8ig)h z93#Bv{B9YIzyY=n&D-kC={J{kcC`O%J@x7^^*wQqoUpsdpz2rxf6MpSU>~G<`?WzH zk(I`x!D-7D+4JYm8?9a)1M!|Syy#A&sn_h6ot+I(JO5A{b3;qYM&6@nhOEGnNM?b3rNgC7dLg?L5)!_?d(^?U{#8~B+<*`)fxApce+4ek!dq^qa6VU|i(_u3 zs+yYZ&YfE*%jNuMh3JG>CA}Uq>md;#>du|_;yqThYjU~S4`GMUY%z^NmUjl<5V{TL z`giT%*0e^QK}RrrJrxea7jxRa_3tgCnibl?B77DBo%=ciT9LUnA)Ox@BvsixT51C= z1+dsDFc%^SKY6vA4XPi8rz-$|pI@HU8=Q{dK6S_k4Z<>O_fRHSj?3+$= zmdJY9Hhx;RXYXD{;?udOqzc+?2v`Mx^f8P=hpzY?vh?Cr{Rg*i>lvBL%%?B4~|sQx$5R00tpiS`9bpKM_LFHqFg(7E|sTZ z@%Bzo#)Q)xORgV)tq;H-SWgS1anAHb27fowxCqP^Tx8FlJq9|zy|WbLiCC3yqOOH5 z>=x9oIII1Q)W@~bKs_Url!o-^(PP?{9B4*^VE zdHCtSrfuj*dqrgs=4duUL1B!#x*s|ho1fn-a5ps2(e@tZR8ugMms0um?l?-*Ag-g3 zusPHetQ;Sj5l`ZhB#2QqIq%*L_A*=;l+cPP?fQEC_H@4RN%CtuM|lGgR2Y2-qW^*h zg)}*E>eMNn>vPZ^!^%vZIdhKUD30aejQWN&N$xAjoCFu0v7A&Y{BiMNinsc#3LcTXH~_R9Lw?E!(8E? zAX+Vo6QbV?*(Qg(u2enn(ufy!Vh_$uk?p^ch6^2SN`;DSDT*BSzUnlAY>guy7ON%KG`h0aJ(kl6FWq z8vkjzYJ^U{!Gl}_>lKd5j`p3Z6g@JU>WOmZP;Piw$EiV+h`%t z;OmCmDfkp1uUyQZL-b{?CbL7{Hv)m_w)}~fc9|EST9)SirxswwQQ(njJHHsg02;;H z1sSRhs*=o)T$e1FBfX|GWNSsmlc!$e_}XP3pL$_qFqn6a?qnq=h0(f03#1o;O8jc* zBK{veAddG~KlS8EKSMMf;AIsyF~{l{VlM#{P`nov6}hN4iYtY;p`l?qsiTrENm5dB zl#Lg|eP^ai4;vO|Q+9BCxI-^H8ynHs_2}Wz*<@OP)y9gC9}RT7xxSHJ+h3HJV3cry z{M3at(q2>p!nh2km{NP-@k=yWM%K4g)d{$ub#99T2u5YQOT5E5`Dx&fsv@I&_61j8xgDPlk2& z%RXo*+ZrI+9t5O&^aGYDm>LAh1Je2W*4kNCr4K+l{FP?sr!HO= zw(7EoB0Qj%MS&kH7Y14+ZSP8y>lG|`v*7rfqQixPXuoRJD#$aEY4(qjsHL*uv%0jK z)*AzZ#h7z);^gG-XbUY5CD4vtizea`}eO<9=-}%G8i=~ z&T}UhT;$}sU5>VUIWu+1B|TKoYt~${DaTN0DjvBZ75vn*h|(P$qYD~>4wL5j526kh z&*a6_U#<@lU7F-bb;Qb72sCYb-K6`VC!E_j1oM=DgoHIUWpCH54$1CxDm;6FrdNSo))-^2HdyqUG?-XGbF5MN~9GEJlhE%;CZMs;gVoiee~mqV~aMpx2t%{-%tY zi-A_({OcVm*F-Lr9nFBp*|VbOyrj0-t6*h4t(M5c^g4csZUCW>zIUgd!AW;LB7y<# zG>u;wI~{@7A_;&I)?~Qkpodd_si`g*`1cUutSr0~qs4d`w*y?^%V< zrG2Jah(@1FRjz_r2aGYpy|3WY$B);)PwiNxGs3(1hRl&*+p?;v<6>4xO>fpN^n|%` zu9OW+FH99vtG2a8vw2)%9MDm&hhs<^{bb*9{seT5#a@fX&RTE$%G|`zx(t(RGT#SdKWuf`q)_jP1B}{2^Fo7{UdqHsGOKW ztX*JP?{WP2;qCYAmI9-B#B8rxN15aD{mWwNZU7oujtek(fsb}i(uNI3fjSzTMvI=D z^IeWsQ&=P7+B)!LB4in?4+uc_RUr#!M-VsU?5}b2H+o z53hjb=@}TL5_uY$nu3_h1EfhVspGZ&{qkiny*hWyqg2(ZEkGsHPj%N>=CSdP#MiKY zvV~mcjqp`oOE0DGCZQZEdetXq^=XA$PQ3y}a+>P8UC)nSJyONKFE0%T?z7u z{RvKQPJ7&ujUO(?lrKEFw8`-I%iP?RafS);@$N(Vcs_dU$a~zjCjtnWM4tBK$x#vs zz7=;m7w;S{vXfvFQKD?2JU}LKa@gz?it|oB6Moc^T~A1>oCj;|K4prDV*;0oZYwG_ z*4oDM&$|4|OBu13s`d`^)$vvN^H?vWZf2}bh)!q61iJP7zgMZNtJ6K*MnFgK7U)=m z&OPs3#V*m64k#ao(O8dDQCVv#?U{CsMyN^KtLT)*TmOtd+h5<6aSIF_2}JsS?;s%d z&$CHZDg7cAcX@4S4c;c~Pa(`i=S*;ijjrzIh19bdUpTaeY(Nma#F<9t^ z-W@T~`vn1usl5ys){cROD_3@_Ud$c;{rk6YcR{Um{K};ay&~>BLcjqu)DQqGb)LZ( zdQcW>9_jfAfE+5%J3ENe@s6uP_TW|F3lneJT`F|YO}>*%0wy)Lg#xsm;nz#2}$3 zssq{$8%IaWtm%Q0fF$CPb<}<}#_Pa=cA#&gM_+TCM<7BLrl4hKR9KsZ}=SpxpD&M7CaM>cNmvz-KRQ% zA_-GlY2A}g)$sM}24-Vu{2Q`$=eXD5E2;xi1Q5Z)cn!_$Y1cuRkpil5!Dy<|LF6{7 zf1>HLrBdaO%kPVPw%E^Dz5)OV*36902#AT?z5k#s%i5}|s|BX{7CjB;8u9gLxyxms ze~pMCXByS<282OCOUs8-Fc7%I4VZi7f<|#`smR)ye&uw=bETh5m@G>H@%Z)Yvy560 z=s2y{xtJRcs4`K&A^BEYECq8laQjPUK1JG$YuqBn1!IHZv*EaaQcw;tPtj@Bs(cqa znl~}`NJXD(XTUK-hKv-@9u1B!L6}Z^fbrMy_Zn`VZ6xK3aA}k^^x55ls@ySN!3_~7 zRfcx3td`C(nE@P}&I}#zZq{eJu$m+V1)o5jGmQ-muq-l8PR%ZL^JY7Fo;Wd8LE&aa zZa~-<8l?K~--9VRlxENFi_sjv95RI$EOdwh=Y%Dq{WV-1O)Gd5t5F@RsDkx?l`XE+ z?l2U(JC5_nPbMeN;fB3=`e|{ z`Ke>K`pvdK!nbgrdr)s{+N=2?DI&HL@KN(!bu4 z|BtG|VZMFUzp#@}<>h9ZHfe##-;RwPyz0U^y_dWGOUzhxHQPev`s;yJ4$6+z-m>u~ z#>VwcO=(2JTz%deB;Wn-Xoco(Kym?B(r(-QLjDp=(`%R)n&Fn!nT z>mVe}2yCh3#e}}_>^Rnbc4HZkMd7kG&S1R%I#bh^R6tl*D}rbiB|4oVKe|`U*)!NM zJC5)(Y2AY>^q8iuZH`NqEt|dT>lgrsO|-(OEV5#)*Fgpb^_!4DIm>`4joOeo`&4Oh z_(8gH=Pf8v`jMo2h;*G`JT?r5;*tYoUt$uG;qH zn!=5Qkp9mYCQbt6p=13u9ou?1o?qyw9jf0q>yzC|hMI`{sihf-&DyGKo;>cpff^pM zMlVPKy7a^3s|R%Ufxp%vI-~>>x0$X7tq=u>yNdSzCUqsq2&e`6kNzoyj8B+VRI0z@Oo8}qwbNGB7tVQXAq=J2WE^Hm1)O#J)8pzbTF>lHQ+55)y zuQy{{dJj_yFqB=^Xx!iFGa3_<|5BrlZ$gdwy8YE(y4kyz&+_36Jv-d3zc9(-%@pW*2|obVR_>-V)KC4NPA288|R)dt}m)`1si{dQIQDnh<|%UU+3yLQN}t zwOk>>k)@{dy^`Q6hpJE78|#&neF>&nl?qP-becKN1N+tzwEr*F;{kpmI^qq>?OpRa!bHW*-F@*j z-ABk7Egm6kj+;%;oT)uKSChiyKlP5K9|w#@d3fFIe~?8UXHee;p^b~7vf8(naDipO zFXQd=6k6Qj+(Jo3d zVM9|>HS4jip>37_u95s4_{4SM@3T@H+_he>H%&i6VJFwV z@N>hLtPAGZm;R-N{L`7*rEnqg!WUlL5T)TSv#-aH5U3XOxZ2p$dvM(;} z;S!PkA(9%?MU=b`(BG0phPUo=ecH?RUvl7Jon&9N-8;RCk}&(bILGzDqO-pz3DZOx z*M{d+*7D>x>GKq_mTYXJZHBs}@Rvuy_%{{TO9qYnggEgp zqcv;3G9PG0YHW%X6MF7ni-%B|UgS(iE#7>&1KjP0$ADFlN^yG-GTAsexqhhtoY$?f zzZ9N4R6FpyLWbf%>UH|2qauUhK0l}FXOpvC%eu0JUN~*4 zqK8$<$AC59HygN40;g?CdF533+6}w8mDjDD2i=BwV9I))ocAc7XF_y=*OJiWK-z%N zKxd2`H?G1xT8PMijy9T`FCqk@p`qPcXDq?lus*L>c8Z-Jr|42VRBzC|3c(L@^It(7 zdZ=9)es83zl`FFj^{9#On!mcbAn5q(=$l%j%v$QpzE$P+Aojx`zeRhOw{_f@F*(nC zeKD4Tu0h^4meB#EYp0>YUbB_J>KE=g!$~p*-Q@bsk<}2+n{eR8CILZyOpZg@vxmFc?HK zJ?p~p;Mp0lRsCdS6>aI^3~K>~1N1f2*S~`@*-;qN%Uw$Y zZ$Fa+heyw!pP=SwKyB*bV0&qVx3(Y(I38D72t=Abv#sN$yv${8e%1FrtgiHSiCEEF zEq|s=f{m|Y(lRO7XibcUoN-y(pRi*%vO8`AOvWQ(Hai`{7bOOH_VDj~>Hf%==;-;Q z&h$Y)g*{bj1z%kYw{J(Umj7LP_&!a&@>rUFn8C+gB#+17E}W>l%-%(qCZP{K^2SJE z9MKzz>86tVyI|PBip94AAo#^ycwt(atzrMoM_zvIvdL0VjeNySR7vjOVL~=HapIEd z>wYt~o@|L_nt`F30Y9v82*hVre|ZD}|1VYNsdRu5KK6zG@3KP~y$Z=7z40v-bN)=| z7c{X^QPV{J0dLYCdWgCBz_Zx~?s0KSv|5fC;l(-XR{th2ym5ASoA5*A`G2h6W5}`6 zRepa{G2;K!5gOjPl(>TuXYnJ!dCv4XmLIHaJ^@|v3Z=}5ggBIFqVgIF?TFO{L7gmBC$LoPTl z!IR<^+K7>nW4N%3kyxhUgeWRcc_9{9h^ho3PQVxJD#t?K)U@Q|J}bFz#v#&~+L1Z- zzAm5RBO0>O9TPrVUh31Y-$4-8jI98v;w942zJK@bAs9cdC$8&8uVn2~rp zkC~m{nHew8K*(jG8^&qO3~dTy3jTY)|HTYTYF^%XG{5+Xm@KcQfA~nS^sDeC_fJouP(SKs^#XLAc?{2BQ8D2V2ZA6hu#C`e+NM z`WG^|QBDC|YqT5XKwW8BS*K}t>AH~KTmglrAYZ|EMD+p)4kj|{%pwSru@Yd7S3Q9y zkpMz>kC441dy&asH!>7VH-iZ70>oS~jKMmFQuyDN!b1_apZACDuwbC`74|iB5%WjY zALVPO(W47pDYcGp=|br@@L3bw5&)G<52A! zE5VfIJM<(57VCuAYUZ{9wBF3n_(OYy{~Z{%1JytKbCPe5V4Cqb2k$HAxgOIh52EE= z7(@tpVdw+?Giv6{m1$LK$TX1X2%|B*t!j==P8EzxbzaXG;JbAL%Z8jxt-Rb1$O7bh z#l2m?4SP^n&>aihkXTNC4=ZGbhz+;_U&f1T;)km`k4PWz5T(7a1^~Z;E^)tJm+`Uc zl<=yg+eN5V^sX5RiH4l_9rS7UlakUZstUp)PBFjl1o4Y_>m$w%Ftam_{1p*0zN@Pf zLR|zGj|&Peq1_a?TDNX}7k{du;cjlO;2FzlU%h-;iXI|0H`kjUg0LU7A*rq~Hqz*u zOp^j5f%+<$(ewIeiaP{VB2;vU+k`CDbAW>Dk%+FD^|7HcQ=* zOR0it;C6I$5N@158Y}V}^YorXGj(7kd$WjLvTe+QggG5iQR^FKJ8Y0cpj>!!(thKWWZ9&&;-+G#yN z9!4+X?F%zKsP}byn+Af6FrZIu!gciQzL4R~SBz_b2z42uy6Id-8#zLZQ2&=yGyL8o z#uA{cZ?i~cS}o(5n(NmG==G!LNXJ|Rm>~p}f%E%NuiaeL_djT`H$owT#Xv)NAU75_ zkK9oKwA@tEgb?vSg6-%ti&B5-$6O1vmG#yZs%k_*&{TI3sPM26wR8b-%z2Xu*GGl< z(2->lS)nUduU$K*Z^U-VB>hZB*WM8;zE{GxPMAD72rKjZ^(j4sd#~&Ehz>7YE(bex zbi7FGM{`hSzsbrDjU|5-|RS-}L(MF-kC zb;66saeIDDrw2#hcAUcs%qA}{&*$k2Ld%OxLo-K`-9TmnLewAcz%vq-#ROFIp2H~E zsC1vac;QJWoZo7?+UG3u?DyjwD*N%~p&Z{KC?xTCa)Y5}%Bexo!)4uAIgxlzrb&JH za6cwtF5_mP=7ieo7CYeIgmkUxqRILngmjNyzLX5L_eKZ9(wId?eTUan<0T=?S(#S= z)BD88LNF|noB0ZTeWSZudJ*!*C@P-kb-Ro+D@dQup{LKsghI&FjAMkeHn^kI6(gZX z-ppK%Gn46zo=50xri2W$)AM42oT?0;elu!)F)Gi%m>BUPLR7`yH!?CJ8>S%63jg{8 z4q<{~DqFnWH0A+AC;e>400a_`S#XSif<1af&|H%4fLd7q_)r*w!GWRiP=0z#i?$pw z=-|WFn>ULhmQ((L1Sn33(ICXA=7ezY9GrV04Ks-?9gmVV`^(7aP;^rwE)QF1qLSM* z?IGMM2w{2YpWn0iP9G_PmDipmih*DS1FeDd9);0r@2F)0P{eH!Mm(rp;1fl|Z^r}x z4S(t*e+n>R?944ez0jGS6_Kb=aKq>c^%je5x~XW4>;9iw0GV%~qYy+_dtLezACZ+h zgO&pFu&lg%?0}t|seP?Avv~Pb?>Kng6^@PfEFKG2Jv2RWC>W%b9o~sVOthlu+IBx7 zzl#X;!`Dmx!g0goNh6+@luOs<<&}t zqj}4G_c1KS44Pb!ry82r{wt1t8@6v>LfJzt-gI+p!6v;~x)R_-!QV&RK%k8x@;ow5 zQiWSepusT(Z^xZGWtlwr<%`aZXMqHblH8LQE&L1gjg6NQf_|V3sZ!fW7d{LdEx*JH zda&i=2W+r}NmH4tHa3cfN~WL=IQ-sn{rdg9e!U7mviUg4EtX8sPMr7{Nd8)pHUSdG zf5N1h97fcbQ}L%X>nWxIj=!G6M1F!(oeqp{#>{?>A8tZKz~y@Hy7Z3#?{40{JbROD zN4a-lHYO{YwAJ$4)Z%LS98@dbXm&);Fk{D?shF4RTMzqH9H-+|5?mv_cuYUBPY#3J zf&eEQ5HGidAGxJ&rjedK{x8v9xx4W|y~L3_?d-%BGhPopOA5h!gTXmEqi{X9<@Y&a8Fw~RManBng?#0{ueJb6;%=OO32clKfg}<1Da-!#s}rL z1amK2GR{qqD~Hb{U=JLWcl1^C_Eo&oX$ZKe$(Y&bJgyYaf(uBqz_BR0DWS!pG%_K& zWxB)>vX9AJ>^AAIwGJ(2T?w>YFaJGXyD?swaw!&j<$#yI^SqYw|0f~F2G5^g0620$ zIcTn|R|Bsyow$A5w&RqO$1ks*Ki};_2dCO1GZOQC z9MHjnW`&Mnj0-ucN#>mhz3~nYX6-}_wkFo<1n5wy-g!2N~~&tyP>J-Z_S z90&q4KcCFjQ;BY}CAh{*{$mot4;o`f#HCgE_Y;!@TSKM@3}@sPE?`}i@H z#0AwC1ZXQvCdD3KpnH&8{2Kb!EVnb(RPB;rTSM8vNeKeOVCyLArE!vr?lttwN!nO$ zc5eEpqy!hnKNK>qTRmU9diBh|hieNR5_C4HT^vTfcvIde%k44``E3aGn9}m01jnXJ zPj_+7chj5~TSFWB=|7PIPCr!-RH`i5seDC2DK9kM{vrx7`pq1**wz{6g{V4??7 zjJ!gL965Q7q?afLk_G@{l!S=KX*dNNx{$p%JvxuJ!e(|-!SdPJ*NQ8`l^ z=JqIRysZ)=D>x5Er^I+D!-55^(#R7>u<3@kld!W!?wkV1Dp3M$WJt>`k}qwKa;Nrj zcIDmaL(`eoqR0St^yus^HV@E2D?8OLrN!i>87J-(LnuHTqH;hp)uC$8fbzMkj`AEZ z?Ecd=)*XO=OA@?NY)_>usQ>ykg<6;R5EVLGSvgO|WLsCdJ^b`;TzNOI`02g>Me!AX zV28Gi2%$5uaAw?~Q{O7Pg|rL(;*BHTk6jPaYj@0{DrW*JpA;^}P*t@?8W-}nr&Z%) zh##iXOs*)R3HD{D0KCiS+Wj*>ZU|XcO8}-9Tn)yr{qQtUEG(sDPO+~GY#T+x7Dxw> zh~IO**Q%)BkozW^X(8kUO@hmwv~2CTS+Cny>B2I~D${ z%d1vcQ9FrM;IL{E2~sm( z2D^M~R*h}A=Qo?_Zg3SbKz(B9Ozk0qtk<^XD&Bk+vlqi^3BTOv1=spgxC!wyIN?;p zK$!nX?yq(Un^Wi*tNSuzkGXm1lcuA$A{c_2nc3G@r=tFheZ==ixj(+>z?tx24nexH z)n&Z5_Nj{Z(Xp}3v%9qGT2Y2(iEVmfr%{=#lP*d%tf^&@m@t7BP9egqiY`QD z@>U8!rYcj+yFGlMZts&RQJwJnUf2%b$*U3~I{eI!_>#GEb^dIpvDFDNF^Xaok35f6 zVqIffb##P%p4Ht>FHW93V>Brp3G$cVH?wg|Vrq57QmHpdyJHIzvVtK*lzk-^Tzh5l zBVmBf5+IFEh7rc@l=1I{`#082)XN;YL)fw7N?n$Uy`q0T9*mE8-ASiAADTB_mEmSQ zo%-)9%qxz}u4L6T59SEa%~2;s?kDw;yFlI7sk~v2V{owS{Xf53o$1AFKWB|RS|~6E z)?s*ZQ)zQQ;04qt#+8gGy0x}~2eS%NK`uT`@aB_GKW`^ZWOmMHtkk?8Z)S$g~ZI zO#&UG+e+}u1)A+UV8EE`lT}rV$`7|e;hXu*(~ZcO&mn-pE#A4mwzEgt2HQ1%-7t{D zfu$$uF@+MQZ@+${CY@l3(A?(YNNU2sSt_!!3qlRFM)jpfIZOZ+M*SD?tlbZ0c zOKcp?1oil2cpApqZ7xVAnU+R27Nuvq1QW`LgW`?}UR>CIFS?w&Ee&l;aozk$s<*Z? zGjWXVGGv0DEX+&U`g3i*a$!XHUb~f_E?>SZoj3fXQsU#fs!J7%*a(>I@-o-fcBda- z&c`tq-I5T)i(5O$BhAWej#KHahzM2<+^koYmF>*+(Ae(?bk$?@e!P6;f8m`N)B2lf z$9m4Ga$DPPL4(6OHIS>?eO5oB)>xXJyKlR~P~lhI0{+)pbYX|NIWpHf^#ysF6*sp& z-4>m)EjnS+RvaF0uek-_Gw4Y7^wiX@)eGY?w&ppuS+6j8kx*+FJ8^Zs$y&!^IH%LB zNI-VAp)AEsq~w~HkN$Q0>>W>lnu~2MbFduT=?ZT>`FCsbpTdX`>Cf`IF$s@NOaQTt zA<6?qCb)@c*aA+P;C`lEqhB2xK z#I?>Fi$jk_6fJxH^ywqU+NYxG7P`L73#|J%SN|K|^aGG2IB2~oR?B$yOq0^ZD!FYZ ze`b`D(wm4U%&qoEh0w=c3!FQR>-VOjuj1V1c}&Nu)IMmnIvU<7Hac2-wmAGjmh0<} z>Zt;jI7gT;A^ILMetcJk%q4sHzW_5ktXfq=Ev-ej#t2MZeH$8oaYD)eu~N!nz(}lU zUbJ+n7IIc{mvl*(z3}@9y>X-8AW55O-3?JgSjdO7itc<{CM5aT>j}X`7fmCh3(rc5 zOLo|=#wz_7AT2f-Wif?T)iajLO`6nm&8c zJAO1TRUA}sg)ef-4}ZVvljOgwg4^pf8`Ccor+2#Jm#9z)l2_ z7^(89q;R}$XG>o#_ayxSj=aOK`T*J>^rXGsVlh*-J7 zI}09ekbuH3%m_^_F)XZki?yg*1ptR7%3QNJO^BsuL$nc$ECRY09q} zYgf%xs|JfaDUmY2-Dmj@rTbbZgX2I`|0U69(4Yx27wWrM-u%pCHy_79j)qA$n4# z1YQ*sbPv);ao7{A2P46LPQ{Lcl~q)h8ybGPbLDNpFi0>h(vVd`r7Tago91wn@)^|$ zR&5sliYL(%i!FT|i*qBZX_&>i6@a>c$6l3`ST*aSVp5IH1}e$l2|kqxoB!n=ctTtG_r^1`|Ph(xvn<) zcZpL18zUl&yE^~&tJGu>@k;cDD5kp zlLhC2tN33$>Tx<(9s(`2bLgFYMbUC7>_K4eHWNuHQ7;Hs1aSRn*|uFw>ZAY--DaSw z%o^f6m3(mw^RyA%VfaB2k$oklPo6sEr3LaHuHmt~f=`2YDa97gF`xjJx;G}FqDz(kqk$|@<5pOj#xGDy?| zTyEm4xZII`m5DT5;Cvg|(Du2iDh2q?t+ej*=eeR7x@pvoab;QuBMS$CyKX!rr^*k2n3DJdgsLXP7V%Y9#r^*v#Fu`wO4}3E1RYf z@t}vptoG<3|5@I_0~SIj28ifhkBZe#g#7()cPzktng2MI>6*Y{@M{H=vtF;8ez9k! zYGY6K-k4gY(H)z`EGH^WF#vArT-GV171642ia}#krqRVAQXh1|!|d@9 z;ZZ?b5gHl!1R7{k&rCmiNAeU9LW0rGKuOBr2 z5Akf0y&v-;IkqyXSCZ+Jq83!~kJ)l{fJ613)faQgOJdVX*+C;?;}h^+W-R02DIIyb zDTQX+t<=fUZ@9|Np4_hs*n|rEOe~;Bk01YcS$su=-)!cj5Pq07zv;pghByK>lscvQ zwJW19xb@%0Z2xG}Tjo;x;EAs=e0N@SQbp?8NrV2GPzqk!_ef!flKkk}m{veT<>GOx zDiXqfK3-89v5Rwa0mZ?fNk$_StSmjX)<#~gnX^g+mOJntqMQ@26XWp{^z8+nsXss{ zr|}v&wB6Wb25g;DuR!I2KRZ&LQ+(U*nV)`%En5zys)bP-k50WO$e46w4d4nL*?dL; zNdXg*w6Ns%Ev2=lrYD_Bb}fvH?=aU{Ark|9BF-STp!VX(*vFURGDY%@mb&E3$bcwe z7O+&Pgn{59Hueb~3S{^2_tWr{LJy430YAFTs4&4WKjAIpeS1yg7!WeOI+9 z5nnGln#9s6L~Jr~3M%mS+aV&$t!k_I@G9Zejz}6tQSIr}@lr^#s#bQYQM7*P z>xY`u?PMT@<}cX$dDI z9B)>CwiDGI^*~n&{}T#H1HO!uWeAvm{5MpO+!-G_2+ailyqYqw!-jwU`>3yrdmi&8 zhNO({EZ=3}se=gx!-C4}>Oz)f9=*6*0}F^}soCY@Fg&m5ur7RG*yeZJzW9LJ%7l@* zzsN+FeX&1j+@ooL_@TMqo^9W2DvKgy9!1su+VvaoOS1BhpV-fRn4(s>-E1Zxl3aGy zmj~x=W7zJEdS!LIzmLa)YmXxLHBAih{8~bw7n1n&mVwt_nTuKHvS_VQ^U%g)t$W7X zyVEVh-HVg!ZhqNaW#`>aH#0Z9e4BR-N?|i9cZr0|O8)2UUq#%9FMd4=z1G|kD_1f# z`J!JZSam?abr8Kmx89a$ZRcXSJy7>ebn1qvfEzb5%I#g}kLt@|8rC_9mwH4eq_Hag zkq>h#8Wa=^*KBXLvHEI)tmyE9YxD4nz@%|~^UV#`@p7%JE|06In&PP+rUj72!IY4I zPMp7V>50e7htdWRStP9w(kEXF418oUb+1~PqX!LxYPsSFNrDskgSg8VKc`(jcBWtC zDxoTrQK&s}&FlePteyU77XjRl7rvSe3nJ zakA6oUXI^C8n=zbVSE8`hg926LRwlHgn`6e+ql{z?I36w%c|4Mti64FMo*vqy8L!k zb-T#5h+{rZ2+15NNnMEr&eWcEn7X7Sqk}o)<+U;Ym?W!?_w2bpbzMYIP$ybql0th4 zpw1E0kfY9YB^(HNf|807h>A17Ku)9=nqGhke)e2WHB~i4ipD9_$xJqkV!IuieaPIW zi2f3ymPgA$-e{L(dJ1Nm4KAkVPh>oQt_4O)T$i?bvT1rd3EHVH5^S4Gp&b`p1ghqv zYKCf!OULw^7m__MGJmwH>M))fJ=yq4lMX-KbWlGml_H3tEY{cye9bt{rkw=F#YfRf zGZRGI;QD_6w$`{r>8Z!Ce1z)-LClkW=|K;JE=|Vcq67|Ck2i!9AmwzluoA*&4YT`$CTz$iZwZ#4?;u z3)kP9^hp`TQi;59qyUiraF%m#8M3bE+U?tY03syz6M6Atz@a3NpUh{PI;7{`eVUU7 zjb=C<-c&+Val6Iv`Zxa|vH57bjnDi-1<)ICYrI&t7C^>)Qpi{)*XK8qE4dATK5RPfPtb z8u4@QiW18K8#PEC49UAGI2UM|=yz_zHK3n#aHym|8*>n+8?I8gu#HjG~nS9F9Z znZh9>rkU-B2Rw=qW1y-ht*x)GZ=7?()#8dB<-$k{#2}>(ZAov+1~4Z4$PQ7((GT+& z#MXL=;-nSGl+AbS&_W*tHM`uvK)9=kLpOW&U&_8ZEfM1?D<@2w)|bDo#Jc(GEJS#i zzb#sPOQJ(TVd0r8S5ELwaMn=z<%`qU6%{ARxlWmZDkbOlt}^y+=c{uRfSh;20)&Bb z-i*6wbp5%zyQE|~pe0fCa9NaMOeuTh@N7A{RQ3>wWAEOiXZO!O-6Wn4FQUe({!N_n zmfDP+%)!8e?B^2#)QZzY!y{qrQ;7--CbBLU(1eQh(QC{UFPBwR zP)AnXHY;J76yq1oKO&v#j!&$O*%)PX_>V+awSQhOy}l@hVEx8AT@EI-gpswr(eW3=-q86D}sZP zQ|&NbGkxDlvIPT!N-s`DM94chI+k(ZaflNv=hwu$cUOGAwQBr;osXCH>U`+5dO%_Y z){dk+W9;k>uyB2qi^RiC)7juSZ_=P^;o(cVb_d^BjvT&7{C=AsYYoG5BWlJ=g#4!# z;AX-H%ePKxEB~5$ajChvd7n@5Yz5%UDM{D7%FP`+?35o#3>}l3DjKVKlxN>uk{0gT ztnxrDQ`+gtbCb6*dt(8VU6M*_V@`g zD@8U(G%1&)U#=D<+7HDLwN%BJk(M#8vlJCwzC8xD*AE*&swD=%VfOnJyn5205YM&O zUYX}-Z)}x*_s6?oYq`;KF;7^k+kAL*UbgUu zec1iUtr2k(J5c?IYmbbP7QMCS-sW?b=2r0b0L6zTqLX)(*ItM(nBuP0KHj+YTT6mm zHdsDkRy+&rGpFt;GW_J?UmBDDvZ`x%Zb4+TOTwUP=gYrMW2wmgZjSRMEqb`cAK)&5 zAun3I*j8n5_}09)=; z!4h8oaQqRji-zuYYrpdYxm_Uis65|%P@ugB+-Gbu@WF$U>b3tzQ(g(CFvTc`cX?5kAn;)h${gZ$nJgGcgNkJh#A+b(4?0(u(`}S@Z{@3># z%gw4S$+157hFWrS*n4{V?Ai9wYkIk9jO_Wgzq=Npx8I-}EWX}*aIW={rI(k=hUQkO zS2{-fQYfK3nir_oH7SUQB)?lnZSynDD5Ed8n`H^EO$<4g&f{B!L1@q-$H`w0(s{cAI{nd`xO7C*)@JD$Xx;@$94os@Dg zSsF^p6Cft_3}M&i7b9hujeXvk?4oYp3?8@B+fVQv%S*j1`%a+N=2Mn6!jSU|H0X!; zYvS$RpLua9-&(S;q95$ zl@7`XI5x1PS5#ydm!{OYR3nGx(xnIpbl|?muU`Rd1s`YDbvwIn1VQqk`o-x=dTw#^ zxxqWt`q;%6Rqlid7e09G_0#+Ol<;Yja5a1uz@R{sk*2iYpEcsk#RxW!cAc2@-FWVT zknGtewlNj^&eNOllfcN28cjO#%6x!f*Y5x(8dwrA0nj1J=md9yf=K^DV?G@|>JI8# zQM~;NnPB~*jL$%}bn^p7j~qGA`TkP6AwVO@!HAIZR3_{tzGM7yKk9!O@!9GiAog!h!-lVE-?!5a%G;hvS2h zylFyLDJv^8Ty+RYshfvX)5oU|cCY=8yD4dRT*uplaOy5fg?7jShSEHBEp3<3;W4-? z)#s^0m+=$Cd$q@pkMfZ&)h2$%(leY|9l@jNC&cR@*6=zs_E+Cn3Ry!m>EW?yrGY`$ ziup*3{LRY2sdbKCJbL(Wz_n|S%O1#iYa>UTZT#@L_x7NZUF1vuX*%~DGRGs`kibB` z^H-UiY(#lNAX~L#d^JZ5K+vk${DNQVhG{S>_+l4C2Bv#8jgT}&dB+Lny*)+QAQ zuR}oH_sCL`FYHfOG%nboh{F$ax^HV=8MIOBPh;K=DcRf$OZ;$FqND5DV@Z<&TnxP3 z>`AKtcwljLR>_W|IsX|%Vqk>JE;)3NNrJFzlU5moC7r{Yxk;~CzC=&WMeFLT)9ler zeqSZP*H2ZaX64%l@E*hpaNg(ru_U;oDJNM94(H=29AMcsJo zR?Sw+qC&rF?T;rY*b^i!zcT-lf7>SED>@)SF8WpeJt|M_K|&(2ECq}AM>oie0svq=UeV(jsCK202_ns;#_7(#AbeHhv2B6 zV-xO;wR0ya@lV7G`Q67L5zS~(*zxq9-J}i95tkQ@xwE#ian0y{?i}i4l21GV@t(tG_hSY1h3saa8!SaUm>}^<{$R1s`EE_Md&mz3EQ^1!?>$L7 zEaCgEv%@(`!ni(C|9FJjs;|R-jcvGo#rcv?vcaS!ehr)cxGr}5aeUy!A{HuaztZgT z=`GWL^=~$wBAb~y8_w?gD*Es6*{Lo`GMx{#o7=qY9t^f7eHCcDSd>!sBRGF|+E#_3 zoIba`j=@{^8E+RZx&mwm72&UOxs-6E!Ig0|&=F+plgg0tlc?;xyK14l{(cHaTrG(L= zB`=g8Y4%q8Xp<&}V2W!0%8Ec^wH~ZlaQJR+CsZfQa!E41@C>ToL(rUi)$i_Y-6{O& zWr9Z2Fo=!p!at2UwUldh&hPE6zd)ZU&6?em(T{$l0@3JD+`oWq{i&U3Nwa)KzqnG#3cZz%je- zum1kH+-5>~P{{9dzF0g8;tyxgzA?QVhWo%uy??va-)+&D6f%r}_dFLI;%B@EtWM{5 z*)Zny9{S_S3SG6_t!DSZQq7Q~ws4w1EZeE{%|5?aODn%$Wc&BDt7RKv*()PjV8l0q z(}js+_2AEm-)|O&!(d;Hj?S&Q{mJi#?X6el3Yfu-5iFr-)8*wQor-L`Fi3C&W!a+* zQFEzMBlAD7QSAVj9>^5?u8+g)zGJ0mbm1)8Xbk-dI}IxN$jr)c3Fmvus5klOf|sTP z6P*BLidd)%Ln4~|oyo~8K!v(h>iK?3+FI+RP6VagH&%%?ic7C3_0X5cyR!<;*S6Nz=czLuM!JPJg%6xn4ul&|9@$LOctMUi(pFF)H(f)d7bn zi#9M4(!w&)qr4-KzU*slb*r7CV`AO|00>(RdO9%?x|3*nJGns;N z?;_(Ymh@1WJy~fz$9aXROI7Af${&pEsuCORBmtXZ0>u_dCK|fi&8D+l#>* z%lO!u6R_s!?4wYoz?-CRF4$i?aI@u;5O5m!nwD(;--o$FWL1f|%{NkFGC?k{#l;PA z{h96mWvHUWRQMTT(Fm+dM%BjMwM_@jqJ!tkIOI36&Bra7jc7-SU$F8$w#e(nMK zVS>Q=Ro2_Je!T1c+Q2wCb0Nbr`K#`QNr7OzfoDch(9@0)zXk3qkl9z}P)diPkDi$Q z_4WHO2gm!gs127(e|1LlK)@GD2ktjy1EYyYsI^$H^t8HUjdhjm!@U>xK_X9MnoGcE z=0x~Og0jVOe9ib~5lsb;io1@%_;C^6UM_KRd<>vtn{VPYFU){#*@Muj_N9Wg16Qb=j z@*|eSw=LLI&&Xot5$#l^lzI8zEHl(}h`%FFn;V)+>l)3SZK$v@F zVi_QFN88yQ0II}O?3ZWQ1gjZngmeTJU)sMHf;(V#t7osvZzP5gz|awhN(?{;`JfPw z6RSp2$Y2Sv2f?c3@UfMP{ixZN-0yyDw_yv?%0*0})W#f7+rS&@S_dt@p#?cnb>rIFuN^*H?Lds_#RzE#=I-ril7&@>2#)N?i!J;THfM8K zNXh1($g}8OXArid7-$fsc-skJ53T_{EgZ$w+qbhUW>>y2G5sDP#b+E{lyw=6 z3OZexo5u0|#9tvWQ^~``u`fY5bHygj^ZS(TG3CK{)gG;)`a@^bv-6>>*7LM8$kWi@ zy{vsxc(M-Tye`n&f>>0uLN;1`=Nk#Hg9KH)sBXo&$dug#FeK>@G4>UoKd}v83b$A0 zsqCNtuIw%>kcBpeodI=rg(Z14>>9l=$5ow5RP4y;_>rEPo8Mo3m8q#D015CeD9qeZ zXVx=EAP{i?i>0z&HpM%gyd-Umoo?}-L8W0yPW0Jv6rf~Gzqg$N@Z1!X$}4k+!hJTM zz)j;A$h1W%_<=6&P4sr3XEN)yQmMgoqu{2MpWpQuvuB6Rp0>rOufAm{#tod`yN|O$ zO)Y9?`DA}oespVP!6jng=CUq@I|${*^b?P`6dx2P`-IoZDA|nQCNwavJc3vzN(RP6 z>v0qXF7C;{F5I{8!DA^UzmI>>09-H49J!Bmmm-t>YKn9nSgW&+u`%alY7vMliT#;# z{tUyXJ;m2NV0WB%O)|UGF6?Itro99(M8oSPQ_SFJgcy&u{Is{Xn^;Gc2IH{fqM0LHNS*@g{Ew?5-ejtV)J`)SQ<6PQDG=O z1Up75fN3yKBHD;uO$#e`Y}*D;arjv#btjx384h4sY`vpOvG@P6)MrJeWx~&%|6hA& z9+q?d#`}yd>ttrELm1gI%95;=2}6=bR7R^M6cWnPKDHVWk~PW_rL<6Kmyn3Gd$b8j zrIMwhQk~c5$^6dmY}a+pf9IU**EN4MkEiGPem~#O^1k2q`+nb<#XiDE7aJ6FyqlD2 zPnj*;bwkwqqoJ=J6h*)WKbgw69RkRTM30~ou-G)5^D)8h(fDX{VRxgWz`o2ZTSJjyHp?6OEe)4u&B$6PA&jq~s^4z&?mD%B*4RcWPDp1um}@mhc$3bRxJQ zOTgM62DigXr^X2}&tJv0#%a^nsrrzfK+)_-zVj8bzUvNc2>#|`DJmCJcLfbK!Y<~J zIVQ(M#B%lt2`g0wf#bpf{hShAMf&mi(qI~sKDfHelULh+A0HP7CXtERtnqAG&`C}b*wdqww24zqv~<96zvlh&m~qOg zw4o48o{%1MBteRQ^)j6M4uKUh!M?Vpu(qw!N3OzSO{k+y9crc^2t!1MY)W*R7q}0A z1Xa@8ZL-uS>(RU9jG<1O=xUKa8#;bFMCXMHPjx?$~R) zv&mh>$AuOxcfJ<-mgSjGkhK^;e0Xx@tTpp3xA`X~4%r%Oyt6nfYfFAPNY^yc&enG4 zIZyh$iwfsE8;s^WZ}1&$aW&f4q97yVdqFCIZIN3*^k>Sb(={19<%UbzcksNz<5}(b z{;8JN^LmMGY*Tdceh#tWx4lTooe`86z>_ddl(KJ6+IF?P2y`fNqvE@q_8TVgdd#OM zGq2+slF-^zwbyL z)b*3=jj?avQM*6xhEnG~Ss52fI&9H}ffe}$Q5NM^tF7<@B5TcnoYgCllI^(i1 z*TA43iV@NEmuUI#(Xn@ng)^;F>85ic;`rf7&y#hx6jdIQcv$EMsIROjhwxH)li zLQGtr)vHx~nydYyrL)tX`U1Mk+<%)FEb^SV%Nczs0+fqL7l&QZ^;uZlq!zxPn-NC} zb0t>7)YblonbZMt@7V4w{8CLJwky{;h(r*|lbWT`CTY_Hvew%WzzDR~d$R%tjMCPJ z?~>Y_XEjw^5{%us-b<3aQ<6zI5Cp%RFs5r<`G;E+U+#Q;biUXp5~rTb$05%z&cti0ZZT~T%?(|}7j2BD_EQ(2nltYtTqS$Dal1D@A=41!Y-(Qg4ttl})) zemu@t39T-y>|I&_&hkwydw>>L7;`XSnPp!SP{6#r)%@-*@g~eZNhl$Mp(gT$=gfBk zU(&9(Vu`MuZc=k`tzh`6@BR|BIX-}-KOR~GNoU>XG%kKJhenr+3A z_X9~+uttG-$OBeV)JwYh%iG{EncLQj`^e0qIXg}$sgyyMtaI>v&D4y9pMvQJBBo$S zvVkhD?sQFXqke?4!@ctTWMV3>_#8QL;!W1-le5O_&fXFBCzA5;VQ15ct^{=bbw#8s zrm<5<)KZM|s{Z|Tju&Ao5mmLzb}jsW?>+9+*7rD6$W4ln>iYE$Ui))MS;RHbjv*2=reniTuWG+WUnB;0_p4-z()4i7j z9orqY-P145jF8M9b*#QIbaB94*m;U~4^fu>v_`$7Z+Q;%fGB}tZ|G6@&Lo5CM=U7& z@FXV4JHG!-cx?I2w+!EMaz4B+Sc0Q{Cj3Pw1Qn!gM-M^UB@Cd#*<-22tn#v!vtJqw zmJtLn(cC>a1`Xq1sUV$&}F-e5U==7f6v!<7H z={;B$Av_9$^(uq#M?kM;_tLJ@KU#p!=#%(Q;BEBRfAhqKlGd2Z*iX3pz@$0(G_v15 zTyj`q!joG3mNT_GAjm&=6B^5~i zfwKUnO>a!q+K4}6IxI!xPc&Zygk!?a>Jvia4%qjye;E?zok&K{H6fN_N7LRAVG(2b-T!y~GfMJ`t<2CWa@35^#O{#^rC z6b{iH*00Nr4+?&>@~?3JP>S*{U$>wtK{X-jqeX3LqI~yi_kgl)uHmwaYO{JFX<&lS zC)|_Y^|}uW1thZ)aAvCZCiU2?s?+`E&#@4#s{aRhyVBTU>O0?`&)k{l9ciNE`{r0g zsomwBD^AQBn?oIOD)2#gJ8}XdXDRFzF^lsGM!Uu~QO0OkjgntcUN5vXqEnurfM{BH zXT8crb~dF2g;5*&lW8p7GLRc-PlmybRNLPbwVmqa-iWsaV&l2}fV0G>o$}X4@A_%z z(8C->rJ1RIR z=sl?j5jx!2=I)@hT$Ja(yk889c(#kC@7c`&EWvOB(LfFqESgVEaAZSY4u>qN6M;K~ z$-fPl{vxR5MH*a9MOHaUMAKVeKWaNt!*Hs-06|bNg01D=gl=Jlo5NDG&&$})C^Hq7 zuCqUvz9%LT8hPw)y7|$F%cD3VgIe}|C9Mt7rk!6%Rut>ZwN;brB|qyKO~RXJp#ycM z?G<+U8KJuyrl7raX)Y`g0bXJHhv5#716p@Mx`@)Huzt5cPPt`z6N%_GgK^G?^j9Mf zUhMW^6bVi|ZI(w_<@I$q5x(&9Ze)Kn|C5_$rW^XZyh*KNi{rNajoB{31B=rwEid5j ziZhq=u7buo{cRBJ^!qPIZ|z(Q+m3IC zqEMtT#w4q6Lf8&2MyzC3SO4z&?pS0b87rldcL-g-+KIx4m{QM~g0MM^Z+u)2#jyW+ zC9e_*IbZR-+>kDVF^WPP7?h}oYbL?S#=rYVa@}%a-~nX3SI6=Uz+>+*ni&mpW%_tivLWC|0|Y)S3xW1qVp5mZ%f%k zUr&a5I1)WyV{?R5l^@$Yn+GcY$4hyvOy~ri_P_7NCu;2Y!STTR!LNioWAYH`>vPq0 zF6&*7_^4QVMCb_k5hWY#+JFb6Cl33kzyr0jDO+gXqOR>1k)@v^ze@Uk{y_alIl77? zIx)UklCxe#&ckU`vs7H*?!3iu)I|?W+-R_;nBWXcJ8@HiN_5GwhijZ#NB!`(4aY~R zgtAEsoe}LHY@)`eeMY3?M&FT>e!GYgj+{Q7UtSBuoJX#RO2)8dAKO2@qWiJdqHbSe zg@UbSoxfgc{^VUBY3N;g7?sY+nSZw5q3!XQp6)~KD~6-0M!YVx^Wp#pWy{Kl;#5({ zOk9jG=@nN=fO@s~Gr^X~Y7qodDLfo*k+WPBn;kiMQgTk_>QBJZ`NXW0o#Vx}R}hDU z$B-kUdI)M=$erQ#1a)=NB6TQbHV9K*28Z3+8hZ|pWOG@$&mbkrDUz@08S( z7bZeFDVqv#JL2o>Nj6qATLxUlki-|_YKpEjf9+R_5wRwEG!hUKaDF|j?lg!B#4fPC zsL`skN1uH3fzPkk+5yU3Rs-{)?T}hhXC^QCja#HDzEUqulTgXBjuGoSLvCu z>MulArd_GRu9^95TKOfMnNvi~C=9pEfXQoniX68f48kNk+D`469*vL#hM7E(42}xu zM=wxig8Fos;uaEj<`2C_%ETGkgkHPWNk&|ekW&Qd2ixGNBKYdl9tulNN1RGy$YzL;s7>;`rR)4ZM{gh8w(&-O^&82k;aZ3GE|7Iw zaB#!GTAq($X%^AA%0Z&OJ>yuCdZT*A z=Up{X63xQ9c8_e$Qa?1e^!NUX5e5R*I){xi{M@du3^F*~Y9E{U%)vhF9%Q@HJeC3=lr?A&2EZy#2 zIB=kQOPN_~S?7p+ry=XeudKMS=5LVs^w?r+`spqtK%C*waE&W#s((AG{iYv!V~QEM z{8uW9s-pyTvf`FwOcaRvj_$&{<@%}m=qR1s5WafA{<|;#$TfG1 z=>x5Xl6eEN>ODg`i%+yU9{To#<)x~3@1BYv9VJ<>?LSGsO8@Pr6PDt)*{^LL*{<30 zNJaW`xOJfB*&|2J2L}GEI7z(nVusz_NbO?}{Zr)~7P&M9xXgIAM7%CD^{=0=$d!>qV!T|jk+^WkEYBmC7; ze08Dg%04wl?)tP=-@ss@7okrGd=e!6LipdzTIbI*V~v$AU%6rh_+F^1OA`1gD&Z)U zNkgV@o$KXIKW#JtZ;^0xJk&#cm#f0q86!uQr9$;AT(l@5r~?JEVbru@%Bn*nurm`^ z@5hEM2xmDXdc*QX+3+`gWjo~-6nA= z?Fw2}>(~E*j+Mf~3#2tH)~s3JMcO$pJNqKi2cBRhk$%mT#W1v>- z=S5I;w@+Vjk1|>-V=S7gzB(td$F8})TtN#lcK1%--B=MKZc%m9neN@Yd-EWvKWKEk zrJg#2EF0RPQI$pAbCS&`oL$mEd`g%8PcI3oju0E&6V#>V!`f~5nr>CzU4$E5icydNXZR@+* z+?DAd2#9 z8k}7;X3SWDespEJ^zAW?d!ytK_T;Q|>9>o(Z$+G~B|4gQPXcVPA$zMfDR;$t>uL*D-?M#8)FdJ z7V<~>Br@K`l=kLYoZy!1Il+JG*=3{_+Q%k|ON*hn$qN^LgU>L;tVv-kSya?ExzV@k z$0LUdijOzKR91gXs>av58tb8^5$};2JdR!8`Psv4etXWB^~E(CKWJ#L9Qt0Aeb`xt zU4;=(ZpHOg`HLIfwr+^2FaiV0Ow1=UML`Bi#{*#Oh2^N=V2^2h1E~3)X6WS5P zjLN4mgVWdelKj25m)AuWN1j9i<**P6&d}aIZg6im;6n%iW`P%~n^4>%E!M70dug*C z;&%pkZGf|nbgVx8y;w`DaZ60Ui)Uuo1(A9x+Zvl&TpR)q7D$nz(n)R6j(}*uEg$PiBu(i92UXb3jtMTnS2W1^3{+AWb+(~=*FpE@1J$zgRYyA)EOmR551cn(W zCtGnfz}HJsmB+JjsUm$oXo{70f@zp&5w8^#5|T&k9w0Q9XVjhIcpdpy-nh?vZWI~3 zmUlanau6p@Vfv$G)k8LNBxMzbEnXDwW(F^yprEDRtSt)dZe6fh%R<^lJf&}r-HY2o z8C*Yzw1zI3_$InthH9Wv8Z>$BLGkQST^mm-Md+x4uqkBubxpJ!de+vV)Ux9+ld-_xy)s?nEbQ_{7PY+YZig?tAGwkaE z#rB3(Bgd-|dDgi4r$$pKB22`4S!|I)^7iIS!l!|xeZ>4T9r(ltUn8ETzKO~B=)w?? zDjf^Wq&ti-PkCEiT^*%sU~mF&r3)mgw5_9!;96(k_0JirWUt0fnVX&c3r90t$pUEC zmM1t{Q5keuSEFTFKaY9jJq;M{gjr8pl~b$B(Kss_aFKP0DY>(wh)en%hq?sFq2bLTz3sWn$ed~9@Vd9JG$W^}6A=YvnP z)2DTeD`nMc$Z8C|rC{Ws;IM8nw_BMEnQ`8CmneTNPmTA&OvNH+gtZA5HFzNJ<{#Ja<`NOxF~d0e=7 zu>lI=Swu8P-}dB>_+~^k2_}- z#BK{vu!JDBCpfLUG_+*_n1#La!X`6Aef`VGQ@C?L)-pmW>;aso-a zlh2EOK_s`-mfgfXukT?gcA7eFDP8t5S6v@7dGfaKh!=xILn8r&S*VND)YSG25m@>5 zyk{F#aY0P>Tsfe(TWLYaq(fEnjf{Lm59@&foVtd^uJy2b*+e9E9^2PsPw6=+>ATil z`pf>g!^OK~24tV_|Lx%Hwf&$vqn5Tdg5SbDa(8i4a1lvz}7uOMyx`{L_R(MFHuD~S!xp&k%9KR?>Quxy(8IE8cs+Ihd-fd1{J9+ zygQS8^Br5xn$DY-Vm{wJ5CQH|gJ6APf@2Dp^wS;AB#wDC_MW(!&>kx{9Atg4`p-;Q zx%-+yuvocfZS`he{$%fhV`7%G-2+GpVzyl3$l8@WfW?C>K*IA?&_ZC>t#6PvJ>;Ka z*8xVl>gm}##3bH;XvoVS&R{*9f!CAt0hoi77-C{6C~oZ;JB(^qA6C0EwDrB4mp>D% zr4jQF7Q<_sX)L^vS}aa>pjc-_y!%a8*5O_x_H6(}?eTIbW{JPnF@eM>CTl57YVpBC z&!0gQLxXxslGN6m6ny>qMDK|p4N+L2yekBQVYl=uw|j1~qxdpwhd3au{8J^*)}9Q_NUiPhCYTlHkVkfb zD~K9XqJKl&;fk!IMbN}iDNYNFla~#XlO0k@xNrv7@#^jIMb@7!T*Ac2+l=2AB6k=& zN+RZ5ATXHx2SsBroIMz-p1Z6P0w@K_(I?|`)iVGf#fe!s zsr~Aceg*MB=1mc0z(s6Hx=)jaY$QWhLG&A_4`8dy!5$2WgD;2MO-_+us!(Xh*1PX399pV4!egqV#DPzoe~l?HPF3}$WFnM6 z2&&vFw7Mba$fJ<7_wCR-`&0+YJk)W_^-f;^8inB2MgQy;%~SJAM>VqTs&C#8%A#Q zutMDOfGq?!Z(#j}Y;uu1*&N?uxdd!o0x!m}<8@%4wS{en&7S{uCz~gE7C(O;igX-Y zP5~_e{{EZwb#;61$Dc)I8$?wq5>F8_L-FJl6bvdbrDr#~T`jNH*Sf89c}`8QJd~AY zoJcJ_np)b;#4W5D5w{)KccB+5@r#^|L)0B(%p?Mmlzyl{kd=79JUn>b0kw|h zGl{8AA%5s;8^Vy2PrC#Xom%(jvSr@a45})_Pc-%(@u@RwdL*Tk*3F6dS`J_iM@-8Z7nT5q=l%TwOa9UIP=K-4wZ|Xq!a_)bh z9)!zmOP*QFab{nWQzCA{AtbOBSs!(?YRW?MrgHnbyxGg2UaS@IB`tg9f7lBaQxl5Z9oxfTxX1u!M?>~kOk24V(!%04# z)*}RK-U^&qmI(2^{KcjTb!yM^f6YF{ClXq`72%c$Qh?@AgHC9Ky!@dN6X^haM$O@- z@2)okiGTqrrqMrRP!I%e1>;^Q_X;rx7EuC2BRIk;du)a_0OVn$dK>~M*+1M+uQTHJ zgR(V55dXli3*|A|QV6E(V9M<*)X~YKa>OEPg&hL?%sXqCc1Z}mM93?E!+}GGj^|Y7 zBFzq@AaVTi@Yw|4#3;FPv^VEf<6)!8@3xwbHLu3n6_I7GZZ!$@Ao5rvE#V8?zm^T9jeM)T@E&mJvD~XnA;R+a*Euw*S_w-X3!!3gFzwdg#XQ%H9$i?F!$a4&V zBw#16$&VPl=&Cpy%+0M&4WwuEYY*q=&!0Q%k5d~l3Gz?h$Y>m=CDUg5xhKh;L;Pb;FVkn@dCj6{&jN{7bxuzj zX}nANZg=+%%0VK;E-Np08zBXtGJPh$&~&6znOIFU7##esclLjUAX1C1IotJa`*)4) zsB|yc{C&~vB?;LB#7j1%18h8Wcu;k1lT)PcDU$7C?9R@?F82Dv&O1k?pwZQRfX#5h zj{x-U9+gH-uI>Z>7`_R8fsG}h0w!(>9Win%~}XFGaU85?q;R@gC`u598fRQ zq#WJsdp!f?nEd<4Nxwm+Ps*b`?I1jN-EdD2kEIA|6e8L$d!kdnS7-*TfWb!}fBWmY zKix}ql@C!v0k)9PY>k*T4DiK_e;+lkf5t*v>7P-XulQ}$wmtm!QPca^JNx}JYI<|; zw155BlfOzw?c~LsCKJU=FWzi@h=}aSu*$Y|2c#Dr^QZ9*xpnK-LFsUW&sF^P47WVg zJnQEd1_Jum*XFGI_fcE$=G&+}cG`4`tpV0IH#1Z2;Vkhrt{xSYl}pyG3y6!4?>aNB z*Q1T904dc-8(Y79F{}aW=$WgM#T#Xuswyguq_177skwX6s2uau*q5F5{Kjs#FZ7b_ z-$qv`rOnIFA9&qHyw~*>oIu)p8O$(5UgONl^3KxPVYBCa&+i{k4I8>oIy1}{{Ip8? z_Tfv~gQJsdE_OM^H+0%#Bx3}Z5p}mae&3V0=%SPjysfj5(4nKe5T-y&p;F6ty-&bT zF>rzFr@cMeKiGO}$MGprWWB4bFC1`(Lz7WZQOsmLd-dL9yTYa9fJ|s2TWeKDYjsBG zdOwnK4Z8;xBQq3sk}nUct{)=}NzCO8mmY}Tn12yHo||(+bIVK5DgXS_FvArGNVzbF z?VaGLe~HMy2D`MtYon@FPD;J$K)7fyxC@7Q_Lzz zlb9)-GfHWq=|!%~*Ho84a4!20$R`J`;u4Z}=&TAla0-Mae2~*mO`HQWPyg3!q^Ndy zb&tE%uYDvZoC-cYwlyv&NHN_{!;C$XotbZf$T)^!_!?E8dW!IVhaIAx)(unCx>SwU zB{=CF@I-1Eh&_4Y$Fy;<=z>2W;wCF`#mG8#9v?(HAc3T8XjA^$?$QZ*UYF{HEFO?1 z9UsayB*Zh2@!n((XeS{~ec}yo5#m)5v3U8jR@Jbt=9o>msJU^{X%*6vqRS;kmtOwV z)Ip-{Gl+XvUA;_O`;qIDLz+Fs%;>UhWW-(3J5U{-B@>3o=&#wc>AAV@)~9o|D1#SM zV;Epc7^0Oej^Jg>{M-2`!fC}BGnx*3#DfREK+rdeW5*t~4OQ?{E_kWgKD|1|W*c2j zGz}BXKt)`rzJBPU3h3IC0k2)E6^A($Py#KVEDt;ftVyT&4sRk8Wob zZp(yj??bGzw%&d}4z$`>tsbzgSt24-C&dpwqeN6q#}RrL$K zgq!SfZ}0UiClEkR+t-Y?^_=NvR4l!SOu+NpoRXG3_?9eDB(0Rd8zPBk60fRaFzpn1 z@7+Y;1m!D#mL}J1ESUT>mGCQ6lh4qvzxh|_7^es#?}LQ3q*Y=p28qXNz1I8Xt#51o zUxdgH|7;2;jV+oGd1q3Gfn-nFFCzfH(~^kL%b#G#s`YClsUEA18*@?kT}IJRU!eTI z&Z_zLFjuX@p7a*VGi7plVu6Fk-?Hl=V8dPY{Nu-q2-?O9)8vaCe$QA(9{; zU?sA!A{Y1&UE5V24@+BQ|Hxa@8Fq{s4*243=(eHH^FeW#NPQ_R+1rW=_XqRD=IvUw zWWoS6jf1KOOuhE0cAvz`^1Wt%%TmBIk090GHH&f+c*UJt~#sCr(_U+F;wB z)5?RN)PAPrpEh$PJND$Xu2ROheQk=d6Evp+;=HSDeO{5yE%xbWr!VCxEft9u+BY3e z-PSjtx0pFoZ;~6~aiQHE z{rEV=d*x1vMVRXM9CQ1BFDKt|Mmr)aH_;#7(C8>QD=cp6DEvrcG_sU-(~v$H{e(bGzOeVUrXG{?AZU( zW|~q`nlzy;Gj&A!PJR1G`%xw{r~M3->G^m2?m9u?KW`9|>ObS~|L1&oT;8@aBEQSz zZ|L9Rgg;(Vg1dk64dB~+lYacau|OVs!AN(R`R#7%U4EQcq_ZvHnp8BTHE;3UP)+mQ F{{?uG7>fV^ diff --git a/src/View/ui/DebugRepl.ui b/src/View/ui/DebugRepl.ui index 03494a0c..c0fc844e 100644 --- a/src/View/ui/DebugRepl.ui +++ b/src/View/ui/DebugRepl.ui @@ -21,7 +21,13 @@ - + + + + Monospace + + + @@ -44,6 +50,11 @@ + + + Monospace + + true From e06610756a23bb86e545bd016648c09c07b41ee8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 29 Sep 2023 08:33:23 +0200 Subject: [PATCH 025/248] model: geometry: Fix 'sl' setter. --- src/Model/Geometry/Point.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index cc1971c0..cfea71a4 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -40,8 +40,8 @@ class Point(object): def sl(self, sl): if sl == self._profile.sl: self._sl = None - - self._sl = sl + else: + self._sl = sl @property def name(self): From 245a5057c38c430fb2a4b326f335b43eaf22dd82 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 29 Sep 2023 10:14:40 +0200 Subject: [PATCH 026/248] refactoring: Update BC. --- src/View/BoundaryCondition/Edit/Plot.py | 8 ++-- src/View/BoundaryCondition/Edit/Table.py | 1 - src/View/BoundaryCondition/Edit/Window.py | 10 +++-- src/View/BoundaryCondition/Edit/translate.py | 33 ++++++++--------- src/View/BoundaryCondition/Table.py | 22 ++++++++--- src/View/BoundaryCondition/Window.py | 9 +++-- src/View/BoundaryCondition/translate.py | 39 ++++++++++---------- src/View/Tools/PamhyrTable.py | 14 ++++--- 8 files changed, 78 insertions(+), 58 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index b27380a5..a7815e60 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -27,7 +27,7 @@ from PyQt5.QtCore import ( QCoreApplication ) -from View.BoundaryCondition.Edit.translate import * +from View.BoundaryCondition.Edit.translate import BCETranslate _translate = QCoreApplication.translate @@ -42,6 +42,8 @@ class Plot(APlot): toolbar=toolbar ) + self._trad = BCETranslate() + self._table_headers = self._trad.get_dict("table_headers") self._mode = mode def custom_ticks(self): @@ -109,10 +111,10 @@ class Plot(APlot): # Plot label header = self.data.header self.canvas.axes.set_xlabel( - table_headers[header[0]], color='black', fontsize=10 + self._table_headers[header[0]], color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - table_headers[header[1]], color='black', fontsize=10 + self._table_headers[header[1]], color='black', fontsize=10 ) self.canvas.axes.autoscale_view(True, True, True) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index e89e6d0d..7f86fc8a 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -46,7 +46,6 @@ from View.BoundaryCondition.Edit.UndoCommand import ( SortCommand, MoveCommand, PasteCommand, DuplicateCommand, ) -from View.BoundaryCondition.Edit.translate import * _translate = QCoreApplication.translate diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 249a34cb..13d277e6 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -44,8 +44,7 @@ from PyQt5.QtWidgets import ( from View.Plot.MplCanvas import MplCanvas from View.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.BoundaryCondition.translate import long_types -from View.BoundaryCondition.Edit.translate import table_headers, retranslate +from View.BoundaryCondition.Edit.translate import BCETranslate from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand from View.BoundaryCondition.Edit.Table import TableModel from View.BoundaryCondition.Edit.Plot import Plot @@ -101,6 +100,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): def __init__(self, data=None, study=None, config=None, parent=None): self._data = data + trad = BCETranslate() + self._long_types = trad.get_dict("long_types") name = self._pamhyr_name if self._data is not None: @@ -110,13 +111,14 @@ class EditBoundaryConditionWindow(PamhyrWindow): _translate("Edit boundary condition", self._pamhyr_name) + f" - {study.name} " + f" - {self._data.name} ({self._data.id}) " + - f"({long_types[self._data.bctype]} - {node_name})" + f"({self._long_types[self._data.bctype]} - {node_name})" ) super(EditBoundaryConditionWindow, self).__init__( title = name, study = study, config = config, + trad = trad, parent = parent ) @@ -139,8 +141,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): self._d50sigma.set_sigma(self._data.sigma) def setup_table(self): - retranslate() headers = {} + table_headers = self._trad.get_dict("table_headers") for h in self._data.header: headers[h] = table_headers[h] diff --git a/src/View/BoundaryCondition/Edit/translate.py b/src/View/BoundaryCondition/Edit/translate.py index 28b6ec98..d97ac6de 100644 --- a/src/View/BoundaryCondition/Edit/translate.py +++ b/src/View/BoundaryCondition/Edit/translate.py @@ -18,23 +18,22 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + +from View.BoundaryCondition.translate import BCTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("BoundaryCondition", "X"), - "y": _translate("BoundaryCondition", "Y"), - "time": _translate("BoundaryCondition", "Time"), - "date": _translate("BoundaryCondition", "Date"), - "discharge": _translate("BoundaryCondition", "Discharge (m³/s)"), - "z": _translate("BoundaryCondition", "Z (m)"), - "solid": _translate("BoundaryCondition", "Solid (kg/s)"), -} +class BCETranslate(BCTranslate): + def __init__(self): + super(BCETranslate, self).__init__() -def retranslate(): - table_headers["x"] = _translate("BoundaryCondition", "X") - table_headers["y"] = _translate("BoundaryCondition", "Y") - table_headers["time"] = _translate("BoundaryCondition", "Time") - table_headers["date"] = _translate("BoundaryCondition", "Date") - table_headers["discharge"] = _translate("BoundaryCondition", "Discharge (m³/s)") - table_headers["z"] = _translate("BoundaryCondition", "Z (m)") - table_headers["solid"] = _translate("BoundaryCondition", "Solid (kg/s)") + self._sub_dict["table_headers"] = { + "x": _translate("BoundaryCondition", "X"), + "y": _translate("BoundaryCondition", "Y"), + "time": _translate("BoundaryCondition", "Time"), + "date": _translate("BoundaryCondition", "Date"), + "discharge": _translate("BoundaryCondition", "Discharge (m³/s)"), + "z": _translate("BoundaryCondition", "Z (m)"), + "solid": _translate("BoundaryCondition", "Solid (kg/s)"), + } diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py index 377d9aab..77141dff 100644 --- a/src/View/BoundaryCondition/Table.py +++ b/src/View/BoundaryCondition/Table.py @@ -46,19 +46,24 @@ from View.BoundaryCondition.UndoCommand import ( AddCommand, DelCommand, SortCommand, MoveCommand, PasteCommand, DuplicateCommand, ) -from View.BoundaryCondition.translate import * +from View.BoundaryCondition.translate import BC_types logger = logging.getLogger() _translate = QCoreApplication.translate class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, mode="type", tab="", parent=None): + def __init__(self, data=None, mode="type", tab="", trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._data = data self._mode = mode self._tab = tab + 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) @@ -66,7 +71,7 @@ class ComboBoxDelegate(QItemDelegate): if self._mode == "type": lst = list( map( - lambda k: long_types[k], + lambda k: self._long_types[k], filter( lambda k: self._tab in BC_types[k].compatibility(), BC_types.keys() @@ -107,6 +112,13 @@ class ComboBoxDelegate(QItemDelegate): class TableModel(PamhyrTableModel): + def __init__(self, **kwargs): + super(TableModel, self).__init__(**kwargs) + + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + def _setup_lst(self): self._lst = self._data.boundary_condition self._tab = self._opt_data @@ -125,7 +137,7 @@ class TableModel(PamhyrTableModel): return self._lst.get(self._tab, row).name elif self._headers[column] == "type": t = self._lst.get(self._tab, row).bctype - return long_types[t] + return self._long_types[t] elif self._headers[column] == "node": n = self._lst.get(self._tab, row).node if n is None: @@ -149,7 +161,7 @@ class TableModel(PamhyrTableModel): ) ) elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) + key = next(k for k, v in self._long_types.items() if v == value) self._undo.push( SetTypeCommand( self._lst, self._tab,row, BC_types[key] diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index 5f08ecf8..99758c5a 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -55,7 +55,7 @@ from View.BoundaryCondition.Table import ( ) from View.Network.GraphWidget import GraphWidget -from View.BoundaryCondition.translate import table_headers, retranslate +from View.BoundaryCondition.translate import BCTranslate from View.BoundaryCondition.Edit.Window import EditBoundaryConditionWindow _translate = QCoreApplication.translate @@ -73,6 +73,7 @@ class BoundaryConditionWindow(PamhyrWindow): title = name, study = study, config = config, + trad = BCTranslate(), parent=parent ) @@ -85,17 +86,18 @@ class BoundaryConditionWindow(PamhyrWindow): self.ui.setWindowTitle(self._title) def setup_table(self): - retranslate() self._table = {} for t in ["liquid", "solid", "suspenssion"]: self._delegate_type = ComboBoxDelegate( + trad = self._trad, data = self._study.river, mode = "type", tab = t, parent=self ) self._delegate_node = ComboBoxDelegate( + trad = self._trad, data = self._study.river, mode = "node", tab = t, @@ -105,12 +107,13 @@ class BoundaryConditionWindow(PamhyrWindow): table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( table_view = table, - table_headers = table_headers, + table_headers = self._trad.get_dict("table_headers"), editable_headers = ["name", "type", "node"], delegates = { "type": self._delegate_type, "node": self._delegate_node, }, + trad = self._trad, data = self._study.river, undo = self._undo_stack, opt_data = t, diff --git a/src/View/BoundaryCondition/translate.py b/src/View/BoundaryCondition/translate.py index 78aaec07..00077f5f 100644 --- a/src/View/BoundaryCondition/translate.py +++ b/src/View/BoundaryCondition/translate.py @@ -18,6 +18,8 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, TimeOverZ, TimeOverDischarge, ZOverDischarge, @@ -26,21 +28,6 @@ from Model.BoundaryCondition.BoundaryConditionTypes import ( _translate = QCoreApplication.translate -long_types = { - "ND": _translate("BoundaryCondition", "Not defined"), - "PC": _translate("BoundaryCondition", "Ponctual contribution"), - "TZ": _translate("BoundaryCondition", "TimeZ"), - "TD": _translate("BoundaryCondition", "TimeDischarge"), - "ZD": _translate("BoundaryCondition", "ZDischarge"), - "SL": _translate("BoundaryCondition", "Solid"), -} - -table_headers = { - "name": _translate("BoundaryCondition", "Name"), - "type": _translate("BoundaryCondition", "Type"), - "node": _translate("BoundaryCondition", "Node") -} - BC_types = { "ND": NotDefined, "PC": PonctualContribution, @@ -50,7 +37,21 @@ BC_types = { "SL": Solid, } -def retranslate(): - table_headers["name"] = _translate("BoundaryCondition", "Name") - table_headers["type"] = _translate("BoundaryCondition", "Type") - table_headers["node"] = _translate("BoundaryCondition", "Node") +class BCTranslate(PamhyrTranslate): + def __init__(self): + super(BCTranslate, self).__init__() + + self._sub_dict["long_types"] = { + "ND": _translate("BoundaryCondition", "Not defined"), + "PC": _translate("BoundaryCondition", "Ponctual contribution"), + "TZ": _translate("BoundaryCondition", "TimeZ"), + "TD": _translate("BoundaryCondition", "TimeDischarge"), + "ZD": _translate("BoundaryCondition", "ZDischarge"), + "SL": _translate("BoundaryCondition", "Solid"), + } + + self._sub_dict["table_headers"] = { + "name": _translate("BoundaryCondition", "Name"), + "type": _translate("BoundaryCondition", "Type"), + "node": _translate("BoundaryCondition", "Node") + } diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 623ebd3b..08e36c84 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -75,13 +75,14 @@ class PamhyrTableModel(QAbstractTableModel): ) def __init__(self, - table_view=None, - table_headers={}, - editable_headers=[], + table_view = None, + table_headers = {}, + editable_headers = [], delegates = {}, - data=None, - undo=None, - opt_data=None): + trad = None, + data = None, + undo = None, + opt_data = None): super(PamhyrTableModel, self).__init__() self._table_view = table_view @@ -90,6 +91,7 @@ class PamhyrTableModel(QAbstractTableModel): self._headers = list(table_headers.keys()) self._editable_headers = editable_headers self._delegates = delegates + self._trad = trad self._data = data self._opt_data = opt_data From ef672798efefef2a21c26b378dda9d994fb4c366 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 29 Sep 2023 11:17:08 +0200 Subject: [PATCH 027/248] refactoring: Start refactoring plot. --- src/View/MainWindow.py | 24 +++++------ src/View/Tools/PamhyrPlot.py | 43 +++++++++++++++++++ src/View/{ => Tools}/Plot/APlot.py | 12 +----- .../Plot/OnPickEvent.py} | 0 .../Plot/PamhyrCanvas.py} | 1 - src/View/{ => Tools}/Plot/PamhyrToolbar.py | 0 6 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 src/View/Tools/PamhyrPlot.py rename src/View/{ => Tools}/Plot/APlot.py (84%) rename src/View/{Plot/mpl_canvas_onpick_event.py => Tools/Plot/OnPickEvent.py} (100%) rename src/View/{Plot/MplCanvas.py => Tools/Plot/PamhyrCanvas.py} (99%) rename src/View/{ => Tools}/Plot/PamhyrToolbar.py (100%) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index cb9d8b7e..58a2a031 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -44,18 +44,18 @@ from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow -from View.Geometry.Window import GeometryWindow -from View.BoundaryCondition.Window import BoundaryConditionWindow -from View.LateralContribution.Window import LateralContributionWindow -from View.InitialConditions.Window import InitialConditionsWindow -from View.Stricklers.Window import StricklersWindow -from View.Frictions.Window import FrictionsWindow -from View.SedimentLayers.Window import SedimentLayersWindow -from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow -from View.SolverParameters.Window import SolverParametersWindow -from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -from View.CheckList.Window import CheckListWindow -from View.Results.Window import ResultsWindow +# from View.Geometry.Window import GeometryWindow +# from View.BoundaryCondition.Window import BoundaryConditionWindow +# from View.LateralContribution.Window import LateralContributionWindow +# from View.InitialConditions.Window import InitialConditionsWindow +# from View.Stricklers.Window import StricklersWindow +# from View.Frictions.Window import FrictionsWindow +# from View.SedimentLayers.Window import SedimentLayersWindow +# from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow +# from View.SolverParameters.Window import SolverParametersWindow +# from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow +# from View.CheckList.Window import CheckListWindow +# from View.Results.Window import ResultsWindow from View.Debug.Window import ReplWindow from Model.Study import Study diff --git a/src/View/Tools/PamhyrPlot.py b/src/View/Tools/PamhyrPlot.py new file mode 100644 index 00000000..b06ec2c2 --- /dev/null +++ b/src/View/Tools/PamhyrPlot.py @@ -0,0 +1,43 @@ +# PamhyrPlot.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 . + +# -*- coding: utf-8 -*- + +from View.Tools.Plot.APlot import APlot +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +class PamhyrPlot(APlot): + def __init__(self, data=None, trad=None, + canvas=None, toolbar=None, + parent=None): + if canvas is None: + canvas = MplCanvas() + + self._trad = trad + self._canvas = canvas + self._toolbar = toolbar + self._parent = parent + + super(PamhyrPlot, self).__init__(data = data) + + @property + def canvas(self): + return self._canvas + + @property + def toolbar(self): + return self._toolbar diff --git a/src/View/Plot/APlot.py b/src/View/Tools/Plot/APlot.py similarity index 84% rename from src/View/Plot/APlot.py rename to src/View/Tools/Plot/APlot.py index a863f1e6..7d8de307 100644 --- a/src/View/Plot/APlot.py +++ b/src/View/Tools/Plot/APlot.py @@ -19,26 +19,16 @@ from Model.Except import NotImplementedMethodeError class APlot(object): - def __init__(self, canvas=None, data=None, toolbar=None): + def __init__(self, data=None): super(APlot, self).__init__() self._init = False - - self._canvas = canvas self._data = data - self._toolbar = toolbar - - @property - def canvas(self): - return self._canvas @property def data(self): return self._data - @property - def toolbar(self): - return self._toolbar def draw(self): """Draw plot diff --git a/src/View/Plot/mpl_canvas_onpick_event.py b/src/View/Tools/Plot/OnPickEvent.py similarity index 100% rename from src/View/Plot/mpl_canvas_onpick_event.py rename to src/View/Tools/Plot/OnPickEvent.py diff --git a/src/View/Plot/MplCanvas.py b/src/View/Tools/Plot/PamhyrCanvas.py similarity index 99% rename from src/View/Plot/MplCanvas.py rename to src/View/Tools/Plot/PamhyrCanvas.py index ee1f5373..c4bbf4d3 100644 --- a/src/View/Plot/MplCanvas.py +++ b/src/View/Tools/Plot/PamhyrCanvas.py @@ -17,7 +17,6 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure - class MplCanvas(FigureCanvasQTAgg): def __init__(self, width=5, height=4, dpi=100): fig = Figure( diff --git a/src/View/Plot/PamhyrToolbar.py b/src/View/Tools/Plot/PamhyrToolbar.py similarity index 100% rename from src/View/Plot/PamhyrToolbar.py rename to src/View/Tools/Plot/PamhyrToolbar.py From 44ccf99d48220ff9dd83afd3daa1d309e20963bd Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 3 Oct 2023 15:10:46 +0200 Subject: [PATCH 028/248] Refactoring: Use ui file for geometry reach window. --- src/Model/Geometry/Reach.py | 3 + src/View/Geometry/PlotAC.py | 15 +- src/View/Geometry/PlotKPZ.py | 16 +- src/View/Geometry/PlotXY.py | 19 +- src/View/Geometry/Table.py | 118 +++--------- src/View/Geometry/Translate.py | 45 +++++ src/View/Geometry/Window.py | 287 +++++++++++++++------------- src/View/MainWindow.py | 4 +- src/View/Tools/PamhyrTable.py | 4 +- src/View/ui/GeometryCrossSection.ui | 134 +++++++++++++ src/View/ui/GeometryReach.ui | 231 ++++++++++++++++++++++ 11 files changed, 625 insertions(+), 251 deletions(-) create mode 100644 src/View/Geometry/Translate.py create mode 100644 src/View/ui/GeometryCrossSection.ui create mode 100644 src/View/ui/GeometryReach.ui diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 26cdefc9..36fca25f 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -106,6 +106,9 @@ class Reach(SQLSubModel): return self._parent.name + def __len__(self): + return len(self._profiles) + @property def profiles(self): return self._profiles.copy() diff --git a/src/View/Geometry/PlotAC.py b/src/View/Geometry/PlotAC.py index e43f39e9..974706aa 100644 --- a/src/View/Geometry/PlotAC.py +++ b/src/View/Geometry/PlotAC.py @@ -19,7 +19,7 @@ import logging from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,12 +29,15 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotAC(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, plot_xy=None): +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + plot_xy=None, parent=None): super(PlotAC, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self.plot_xy = plot_xy diff --git a/src/View/Geometry/PlotKPZ.py b/src/View/Geometry/PlotKPZ.py index eb7d5e28..f216443c 100644 --- a/src/View/Geometry/PlotKPZ.py +++ b/src/View/Geometry/PlotKPZ.py @@ -19,7 +19,7 @@ import logging from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,13 +29,15 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotKPZ(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): +class PlotKPZ(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + display_current=True, parent=None): super(PlotKPZ, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self.display_current = display_current diff --git a/src/View/Geometry/PlotXY.py b/src/View/Geometry/PlotXY.py index 54e056a9..ce93fa24 100644 --- a/src/View/Geometry/PlotXY.py +++ b/src/View/Geometry/PlotXY.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,12 +25,15 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotXY(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, display_current=True): +class PlotXY(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + display_current=True, parent=None): super(PlotXY, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self.display_current = display_current @@ -60,11 +63,11 @@ class PlotXY(APlot): # Axes self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "X (m)"), + _translate("Geometry", "X (m)"), color='green', fontsize=12 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Y (m)"), + _translate("Geometry", "Y (m)"), color='green', fontsize=12 ) self.canvas.axes.axis("equal") diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 1b055209..0ff5e0c8 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -35,6 +35,8 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from Model.Geometry import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ from View.Geometry.UndoCommand import * @@ -44,53 +46,26 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableEditableModel(QAbstractTableModel): - def __init__(self, reach, headers=None, undo=None): - QAbstractTableModel.__init__(self) - data_list = [] - - self._undo_stack = undo - self._reach = reach - - # Hack for qtlinguist - _ = _translate("Geometry", "Name") - _ = _translate("Geometry", "Kp (m)") - _ = _translate("Geometry", "Type") - - if headers is None: - self.headers = [ - "Name", - "Kp (m)", - "Type" - ] - else: - self.headers = headers - - def rowCount(self, parent=QModelIndex()): - return self._reach.number_profiles - - def columnCount(self, parent=QModelIndex()): - return len(self.headers) - +class TableEditableModel(PamhyrTableModel): def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() if role == Qt.DisplayRole and index.column() == 0: - return self._reach.profile(index.row()).name + return self._data.profile(index.row()).name if role == Qt.DisplayRole and index.column() == 1: - kp = self._reach.profile(index.row()).kp + kp = self._data.profile(index.row()).kp return f"{kp:.4f}" if role == Qt.DisplayRole and index.column() == 2: - return self._reach.profile(index.row()).profile_type + return self._data.profile(index.row()).profile_type if role == Qt.TextAlignmentRole: return Qt.AlignHCenter | Qt.AlignVCenter if role == Qt.ForegroundRole and index.column() == 0: - name = self._reach.profile(index.row()).name\ + name = self._data.profile(index.row()).name\ .strip()\ .lower() if (name == "upstream" or name == "up" or @@ -102,16 +77,6 @@ class TableEditableModel(QAbstractTableModel): return QVariant() - def headerData(self, section, orientation, role=Qt.DisplayRole): - if role == Qt.DisplayRole: - if orientation == Qt.Horizontal: - if section < len(self.headers): - return _translate("Geometry", self.headers[section]) - else: - return str(section + 1) - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): row = index.row() column = index.column() @@ -119,19 +84,19 @@ class TableEditableModel(QAbstractTableModel): if role == Qt.EditRole and index.column() != 2: try: if index.column() == 0: - self._undo_stack.push( + self._undo.push( SetNameCommand( - self._reach, index.row(), - self._reach.profile(index.row()).name, + self._data, index.row(), + self._data.profile(index.row()).name, value ) ) if index.column() == 1: - self._undo_stack.push( + self._undo.push( SetKPCommand( - self._reach, index.row(), - self._reach.profile(index.row()).kp, + self._data, index.row(), + self._data.profile(index.row()).kp, value ) ) @@ -149,27 +114,13 @@ class TableEditableModel(QAbstractTableModel): return False - def index(self, row, column, parent=QModelIndex()): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - return self.createIndex(row, column, QModelIndex()) - - def flags(self, index): - flg = Qt.ItemIsEnabled | Qt.ItemIsSelectable - - if index.column() == 2: - return flg - else: - return Qt.ItemIsEditable | flg - # @QtCore.pyqtSlot() def insert_row(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) - self._undo_stack.push( + self._undo.push( AddCommand( - self._reach, row + self._data, row ) ) @@ -179,9 +130,9 @@ class TableEditableModel(QAbstractTableModel): def remove_rows(self, rows, parent=QModelIndex()): self.beginRemoveRows(parent, rows[0], rows[-1]) - self._undo_stack.push( + self._undo.push( DelCommand( - self._reach, rows + self._data, rows ) ) @@ -191,9 +142,9 @@ class TableEditableModel(QAbstractTableModel): def sort_profiles(self, _reverse): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( SortCommand( - self._reach, _reverse + self._data, _reverse ) ) @@ -209,9 +160,9 @@ class TableEditableModel(QAbstractTableModel): self.beginMoveRows(parent, row - 1, row - 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._reach, "up", row + self._data, "up", row ) ) @@ -219,16 +170,16 @@ class TableEditableModel(QAbstractTableModel): self.layoutChanged.emit() def move_row_down(self, row, parent=QModelIndex()): - if row > self._reach.number_profiles: + if row > self._data.number_profiles: return target = row self.beginMoveRows(parent, row + 1, row + 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._reach, "down", row + self._data, "down", row ) ) @@ -238,9 +189,9 @@ class TableEditableModel(QAbstractTableModel): def duplicate(self, rows, profiles): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( DuplicateCommand( - self._reach, rows, + self._data, rows, profiles ) ) @@ -250,7 +201,7 @@ class TableEditableModel(QAbstractTableModel): def paste(self, row, header, data): - if row > self._reach.number_profiles: + if row > self._data.number_profiles: return if len(data) == 0: @@ -258,9 +209,9 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( PasteCommand( - self._reach, row, + self._data, row, list( map( lambda d: ProfileXYZ.from_data(header, d), @@ -273,16 +224,7 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - - def undo(self): - self._undo_stack.undo() - self.layoutChanged.emit() - - def redo(self): - self._undo_stack.redo() - self.layoutChanged.emit() - - +# TODO: Delete useless delegate class Delegate(QStyledItemDelegate): def __init__(self, parent=None, setModelDataEvent=None): super(Delegate, self).__init__(parent) diff --git a/src/View/Geometry/Translate.py b/src/View/Geometry/Translate.py new file mode 100644 index 00000000..c583732d --- /dev/null +++ b/src/View/Geometry/Translate.py @@ -0,0 +1,45 @@ +# 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 . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + +class GeometryTranslate(PamhyrTranslate): + def __init__(self): + super(GeometryTranslate, self).__init__() + + self._dict["open_file"] = _translate("Geometry", "Open a file") + self._dict["file_st"] = _translate("Geometry", "File mage geometry (*.ST)") + self._dict["file_m"] = _translate("Geometry", "File mage meshed geometry (*.M)") + self._dict["file_all"] = _translate("Geometry", "All file (*)") + + self._dict["reach"] = _translate("Geometry", "reach") + self._dict["reachs"] = _translate("Geometry", "reachs") + + self._dict["cross_section"] = _translate("Geometry", "cross-section") + self._dict["cross_sections"] = _translate("Geometry", "cross-sections") + self._dict["profile"] = _translate("Geometry", "cross-section") + self._dict["profiles"] = _translate("Geometry", "cross-sections") + + self._sub_dict["table_headers"] = { + "name": _translate("Geometry", "Name"), + "kp": _translate("Geometry", "KP"), + } diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index a66ed781..1b334bb7 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -34,118 +34,168 @@ from PyQt5.QtCore import ( ) from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, - QUndoStack, QShortcut, + QUndoStack, QShortcut, QTableView, QHeaderView, + QAction, QSlider, QPushButton, QVBoxLayout, + QLabel, ) from View.Geometry.PlotXY import PlotXY from View.Geometry.PlotKPZ import PlotKPZ from View.Geometry.PlotAC import PlotAC -from View.Tools.ASubWindow import ASubMainWindow, WindowToolKit -from View.Tools.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas -from View.Geometry.mainwindow_ui_reach import Ui_MainWindow from View.Geometry.Table import * -from View.Geometry.Profile.Window import ProfileWindow +from View.Geometry.Translate import GeometryTranslate +# from View.Geometry.Profile.Window import ProfileWindow _translate = QCoreApplication.translate -class GeometryWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, model=None, title="Geometry", parent=None): - self._title = title - self.parent = parent +class GeometryWindow(PamhyrWindow): + _pamhyr_ui = "GeometryReach" + _pamhyr_name = "Geometry" + + def __init__(self, reach=None, study=None, config=None, parent=None): + if reach is None: + self._reach = study.river.current_reach().reach + else: + self._reach = reach + + name = f"{self._pamhyr_name} - {self._reach.name}" + super(GeometryWindow, self).__init__( - name=self._title, - parent=parent + title = name, + study = study, + config = config, + trad = GeometryTranslate(), + parent = parent ) - self._model = model - self._reach = model.river.current_reach().reach - - self.ui = Ui_MainWindow() - self.ui.setupUi(self) - - self.tableView = self.ui.tableView - self.tableView_header = self.ui.tableView_header - self._tablemodel = None self._profile_window = [] - self._clipboard = None - self.setup_window() - self.setup_sc() - self.setup_model() + self.setup_table() self.setup_plots() + self.setup_statusbar() self.setup_connections() self.changed_slider_value() - def setup_window(self): - self._title = f"{self.ui.mainwindow_title} - {self._reach.name}" - self.setWindowTitle(self._title) + def setup_table(self): + table_headers = self._trad.get_dict("table_headers") - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_model(self): + table = self.find(QTableView, "tableView") self._tablemodel = TableEditableModel( - headers = self.ui.tableView_header, - reach = self._reach, + table_view = table, + table_headers = table_headers, + editable_headers = ["name", "kp"], + data = self._reach, undo = self._undo_stack ) - self.tableView.setModel(self._tablemodel) - self.tableView.setItemDelegate(Delegate()) + table.setModel(self._tablemodel) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) def setup_plots(self): + self._canvas_xy = MplCanvas(width=3, height=4, dpi=100) + self._canvas_xy.setObjectName("canvas_xy") + self._toolbar_xy = PamhyrPlotToolbar( + self._canvas_xy, self, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_xy = self.find(QVBoxLayout, "verticalLayout") + self._plot_layout_xy.addWidget(self._toolbar_xy) + self._plot_layout_xy.addWidget(self._canvas_xy) self.plot_xy() + + self._canvas_kpc = MplCanvas(width=6, height=4, dpi=100) + self._canvas_kpc.setObjectName("canvas_kpc") + self._toolbar_kpc = PamhyrPlotToolbar( + self._canvas_kpc, self, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_kpc = self.find(QVBoxLayout, "verticalLayout_2") + self._plot_layout_kpc.addWidget(self._toolbar_kpc) + self._plot_layout_kpc.addWidget(self._canvas_kpc) self.plot_kpc() + + self._canvas_ac = MplCanvas(width=9, height=4, dpi=100) + self._canvas_ac.setObjectName("canvas_ac") + self._toolbar_ac = PamhyrPlotToolbar( + self._canvas_ac, self, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_ac = self.find(QVBoxLayout, "verticalLayout_3") + self._plot_layout_ac.addWidget(self._toolbar_ac) + self._plot_layout_ac.addWidget(self._canvas_ac) self.plot_ac() + def _compute_status_label(self): + row = self.index_selected_row() + profile = self._reach.profile(row) + + name = profile.name + " " + str(profile.kp) + + return ( + f"{self._trad['reach']}: {self._reach.name}" + " - " + f"{self._trad['cross_section']}: {name}" + ) + + def setup_statusbar(self): + txt = "" + self._status_label = QLabel(txt) + self.statusbar.addPermanentWidget(self._status_label) + + def update_statusbar(self): + txt = self._compute_status_label() + self._status_label.setText(txt) + def setup_connections(self): - self.ui.btn_open.triggered.connect(self.open_file_dialog) - self.ui.btn_sort_asc.triggered.connect(self.sort_ascending) - self.ui.btn_sort_desc.triggered.connect(self.sort_descending) - self.ui.btn_move_up.triggered.connect(self.move_row_up) - self.ui.btn_move_down.triggered.connect(self.move_row_down) - # self.ui.btn_end_editing.triggered.connect(self.handleSave) - self.ui.btn_add.triggered.connect(self.insert_row) - self.ui.btn_delete.triggered.connect(self.delete_rows) - self.ui.btn_edit.triggered.connect(self.edit_profile) - self.ui.verticalSlider.valueChanged.connect(self.changed_slider_value) + actions = { + "action_import": self.import_from_file, + # "action_export": self.export_to_file, + "action_sort_asc": self.sort_ascending, + "action_sort_des": self.sort_descending, + "action_up": self.move_up, + "action_down": self.move_down, + "action_add": self.add, + "action_delete": self.delete, + "action_edit": self.edit_profile, + } - self.ui.btn_slider_up.clicked.connect(self.decrement_value_slider) - self.ui.btn_slider_down.clicked.connect(self.increment_value_slider) - self.ui.btn_move_up.triggered.connect(self.changed_profile_slot) + for action in actions: + self.find(QAction, action)\ + .triggered.connect(actions[action]) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) + self.find(QSlider, "verticalSlider").valueChanged.connect(self.changed_slider_value) + self.find(QPushButton, "pushButton_up").clicked.connect(self.decrement_value_slider) + self.find(QPushButton, "pushButton_down").clicked.connect(self.increment_value_slider) # Profile selection when line change in table - self.tableView.selectionModel()\ - .selectionChanged\ - .connect(self.select_current_profile) + self.find(QTableView, "tableView").selectionModel()\ + .selectionChanged\ + .connect(self.select_current_profile) - def open_file_dialog(self): + + def import_from_file(self): options = QFileDialog.Options() settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) options |= QFileDialog.DontUseNativeDialog + file_types = [ + self._trad["file_st"], + self._trad["file_m"], + self._trad["file_all"], + ] + filename, _ = QtWidgets.QFileDialog.getOpenFileName( self, - _translate("MainWindow_reach", "Ouvrir un fichier"), + self._trad["open_file"], "", - _translate("MainWindow_reach", "Fichiers .ST (*.ST)") + - ";; " + - _translate("MainWindow_reach", "Fichiers .M (*.M)") + - ";; " + - _translate("MainWindow_reach", "Tous les fichiers (*)"), + ";; ".join(file_types), options=options ) @@ -159,20 +209,6 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.plot_kpc() self.plot_ac() - def messagebox_profile_editing(self): - msg_box = QtWidgets.QMessageBox() - msg_box.setIcon(QtWidgets.QMessageBox.Information) - msg_box.setWindowTitle(_translate("MainWindow_reach", - "Édition des profils sélectionnés")) - msg_box.setText(_translate("MainWindow_reach", - "Vous avez sélectionné plus de 5 profils." - " \nSeuls les 5 premiers seront édités.")) - msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) - - return_value = msg_box.exec() - # if return_value == QtWidgets.QMessageBox.Ok: - # print('OK clicked') - def edit_profile(self): self.tableView.model().blockSignals(True) @@ -215,9 +251,9 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_xy = PlotXY( - canvas = self.ui.canvas_1, + canvas = self._canvas_xy, data = self._reach, - toolbar = self.ui.toolbar_1 + toolbar = self._toolbar_xy ) self._plot_xy.draw() @@ -232,9 +268,9 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_kpc = PlotKPZ( - canvas = self.ui.canvas_2, + canvas = self._canvas_kpc, data = self._reach, - toolbar = self.ui.toolbar_2 + toolbar = self._toolbar_kpc ) self._plot_kpc.draw() @@ -249,9 +285,9 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_ac = PlotAC( - canvas = self.ui.canvas_3, + canvas = self._canvas_ac, data = self._reach, - toolbar = self.ui.toolbar_3, + toolbar = self._toolbar_ac, plot_xy = self._plot_xy ) self._plot_ac.draw() @@ -304,7 +340,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): if len(self.tableView.selectedIndexes()) > 0: row = self.index_selected_row() - self.ui.verticalSlider.setValue(row) + self.find(QSlider, "verticalSlider").setValue(row) self.select_plot_xy(row) self.select_plot_kpc(row) self.select_plot_ac(row) @@ -315,41 +351,37 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) if self._tablemodel.rowCount() != 0: - self.ui.verticalSlider.setMaximum(self._tablemodel.rowCount() - 1) + slider = self.find(QSlider, "verticalSlider") + slider.setMaximum(self._tablemodel.rowCount() - 1) - slider_value = self.ui.verticalSlider.value() + slider_value = slider.value() kp = self._reach.profile(slider_value).kp - self.ui.vertical_slider_label.setText( - _translate("MainWindow_reach", "Kp : ") + - f"{kp}" + "\n" + - _translate("MainWindow_reach", - "Profil N° : ") + - f"{slider_value + 1}" - ) - self.select_plot_xy(slider_value) self.select_plot_kpc(slider_value) self.select_row_profile_slider(slider_value) + self.update_statusbar() self.tableView.model().blockSignals(False) def increment_value_slider(self): - if 0 <= self.ui.verticalSlider.value() < self._tablemodel.rowCount() - 1: - self.ui.verticalSlider.setValue(self.ui.verticalSlider.value() + 1) + slider = self.find(QSlider, "verticalSlider") + if 0 <= slider.value() < self._tablemodel.rowCount() - 1: + slider.setValue(slider.value() + 1) def decrement_value_slider(self): - if 0 < self.ui.verticalSlider.value() < self._tablemodel.rowCount(): - self.ui.verticalSlider.setValue(self.ui.verticalSlider.value() - 1) + slider = self.find(QSlider, "verticalSlider") + if 0 < slider.value() < self._tablemodel.rowCount(): + slider.setValue(slider.value() - 1) - def insert_row(self): + def add(self): if len(self.tableView.selectedIndexes()) == 0: - self._tablemodel.insert_row(self._tablemodel.rowCount()) + self._tablemodel.add(self._tablemodel.rowCount()) else: row = self.index_selected_row() - self._tablemodel.insert_row(row + 1) + self._tablemodel.add(row + 1) - def delete_rows(self): + def delete(self): rows = sorted( list( set( @@ -384,14 +416,14 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.select_current_profile() self.changed_slider_value() - def move_row_up(self): + def move_up(self): row = self.index_selected_row() - self._tablemodel.move_row_up(row) + self._tablemodel.move_up(row) self.select_current_profile() - def move_row_down(self): + def move_down(self): row = self.index_selected_row() - self._tablemodel.move_row_down(row) + self._tablemodel.move_down(row) self.select_current_profile() def duplicate(self): @@ -412,7 +444,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self._tablemodel.duplicate(rows, profiles) self.select_current_profile() - def copy(self): + def _copy(self): rows = self.tableView\ .selectionModel()\ .selectedRows() @@ -428,7 +460,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -443,19 +475,19 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self._tablemodel.paste(row, header, data) self.select_current_profile() - def undo(self): + def _undo(self): self._tablemodel.undo() self.select_current_profile() self.update_plot_xy() self.update_plot_kpc() - def redo(self): + def _redo(self): self._tablemodel.redo() self.select_current_profile() self.update_plot_xy() self.update_plot_kpc() - def handleSave(self): + def export_to_file(self): options = QFileDialog.Options() DEFAULT_DIRECTORY = '/home/' settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) @@ -464,32 +496,11 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): filename, filters = QFileDialog.getSaveFileName( self, - filter=_translate("MainWindow_reach", - "Files .ST(*.ST or *.st)") - + ";; " + - _translate("MainWindow_reach", "All files " - "(*)"), - options=options + filter = self._trad["file_st"] + ";; " + self._trad["file_all"], + options = options ) current_dir = os.path.split(filename)[0] or DEFAULT_DIRECTORY if filename != '': self._tablemodel.export_reach(filename) - - def handleOpen(self): - filename, filterName = QFileDialog.getOpenFileName(self) - - if filename != '': - with open(filename, 'r') as f: - reader = csv.reader(f, delimiter='\t') - header = next(reader) - buf = [] - for row in reader: - row[0] = QCheckBox("-") - buf.append(row) - - self._tablemodel = None - self._tablemodel = TableEditableModel(buf) - self.tableView.setModel(self._tablemodel) - filename = '' diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 58a2a031..8162807e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -44,7 +44,7 @@ from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow -# from View.Geometry.Window import GeometryWindow +from View.Geometry.Window import GeometryWindow # from View.BoundaryCondition.Window import BoundaryConditionWindow # from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow @@ -534,7 +534,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) if geometry is None: - geometry = GeometryWindow(model=self._study, parent=self) + geometry = GeometryWindow(study=self._study, config=self.conf, parent=self) geometry.show() else: geometry.activateWindow() diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 08e36c84..65cd7687 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -115,10 +115,10 @@ class PamhyrTableModel(QAbstractTableModel): return options - def rowCount(self, parent): + def rowCount(self, parent=QModelIndex()): return len(self._lst) - def columnCount(self, parent): + def columnCount(self, parent=QModelIndex()): return len(self._headers) def headerData(self, section, orientation, role): diff --git a/src/View/ui/GeometryCrossSection.ui b/src/View/ui/GeometryCrossSection.ui new file mode 100644 index 00000000..b0c12393 --- /dev/null +++ b/src/View/ui/GeometryCrossSection.ui @@ -0,0 +1,134 @@ + + + MainWindow + + + + 0 + 0 + 1120 + 630 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 1120 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + add + + + Add a point on cross-section + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + delete + + + Delete selected point(s) + + + + + + ressources/up.pngressources/up.png + + + up + + + Move up selected point(s) + + + + + + ressources/down.pngressources/down.png + + + down + + + Mode down selected point(s) + + + + + + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png + + + sort + + + Sort points by nearest neighbor + + + + + + ressources/gtk-sort-descending.pngressources/gtk-sort-descending.png + + + resort + + + Sort reversed points by nearest neighbor + + + + + + diff --git a/src/View/ui/GeometryReach.ui b/src/View/ui/GeometryReach.ui new file mode 100644 index 00000000..e09d9d35 --- /dev/null +++ b/src/View/ui/GeometryReach.ui @@ -0,0 +1,231 @@ + + + MainWindow + + + + 0 + 0 + 1280 + 720 + + + + MainWindow + + + + + + + + + + + + Qt::Horizontal + + + + + Qt::Vertical + + + + Qt::Horizontal + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + true + + + true + + + + + + + + + + + + + ressources/go-up2.pngressources/go-up2.png + + + + + + + + + + + ressources/go-down1.pngressources/go-down1.png + + + + + + + + + + + + + 0 + 0 + 1280 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + + + import + + + Import geometry + + + + + export + + + Export geometry + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + add + + + Add a cross-section + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + delete + + + Delete selected cross-section(s) + + + + + + ressources/edit.pngressources/edit.png + + + edit + + + Edit selected cross section(s) + + + + + + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png + + + sort_asc + + + Sort cross-sections by ascending KP + + + + + + ressources/gtk-sort-descending.pngressources/gtk-sort-descending.png + + + sort_des + + + Sort cross-sections by descending KP + + + + + + ressources/up.pngressources/up.png + + + up + + + Move up selected cross-section(s) + + + + + + ressources/down.pngressources/down.png + + + down + + + Move down selected cross-section(s) + + + + + + From 11a7486a9ce1a82dc2e8e660c1529e827660f761 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 3 Oct 2023 16:26:50 +0200 Subject: [PATCH 029/248] Refactoring: Use ui file in Geometry profile edit window. --- src/Model/Geometry/Profile.py | 3 + src/View/Geometry/Profile/Plot.py | 17 +- src/View/Geometry/Profile/Table.py | 150 ++++-------- src/View/Geometry/Profile/Translate.py | 36 +++ src/View/Geometry/Profile/Window.py | 325 +++++++------------------ src/View/Geometry/Table.py | 61 +---- src/View/Geometry/Window.py | 13 +- src/View/ui/GeometryCrossSection.ui | 12 +- 8 files changed, 198 insertions(+), 419 deletions(-) create mode 100644 src/View/Geometry/Profile/Translate.py diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index d8137932..606b2e29 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -56,6 +56,9 @@ class Profile(object): self._profile_type = _type + def __len__(self): + return len(self._points) + @property def number_points(self): return len(self._points) diff --git a/src/View/Geometry/Profile/Plot.py b/src/View/Geometry/Profile/Plot.py index 076c2703..12bb9aee 100644 --- a/src/View/Geometry/Profile/Plot.py +++ b/src/View/Geometry/Profile/Plot.py @@ -19,8 +19,8 @@ import logging from tools import timer, trace -from View.Plot.APlot import APlot -from View.Plot.mpl_canvas_onpick_event import OnpickEvent +from View.Tools.PamhyrPlot import PamhyrPlot +from View.Tools.Plot.OnPickEvent import OnpickEvent from PyQt5.QtCore import ( QCoreApplication @@ -30,12 +30,15 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, table=None): +class Plot(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + table=None, parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self._table = table diff --git a/src/View/Geometry/Profile/Table.py b/src/View/Geometry/Profile/Table.py index 2de665aa..d1a670ac 100644 --- a/src/View/Geometry/Profile/Table.py +++ b/src/View/Geometry/Profile/Table.py @@ -29,10 +29,11 @@ from PyQt5.QtWidgets import ( QMessageBox, QStyledItemDelegate, QLineEdit ) from PyQt5.QtCore import ( - QModelIndex, Qt, QAbstractTableModel, - QVariant, QCoreApplication + QModelIndex, Qt, QVariant, QCoreApplication ) +from View.Tools.PamhyrTable import PamhyrTableModel + from Model.Geometry.PointXYZ import PointXYZ from Model.Geometry.ProfileXYZ import ProfileXYZ @@ -43,43 +44,22 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableEditableModel(QAbstractTableModel): - def __init__(self, profile: ProfileXYZ, header=None, undo=None): - QAbstractTableModel.__init__(self) - - self._undo_stack = undo - self._profile = profile - - if header is None: - self._header = [ - "X (m)", "Y (m)", "Z (m)", - _translate("MainWindowProfile", "Nom"), - _translate("MainWindowProfile", "Abs en travers (m)") - ] - else: - self._header = header - - def rowCount(self, parent=QModelIndex()): - return self._profile.number_points - - def columnCount(self, parent=QModelIndex()): - return len(self._header) - +class GeometryProfileTableModel(PamhyrTableModel): def data(self, index, role=Qt.DisplayRole): if index.isValid(): if role == Qt.DisplayRole: value = "" if index.column() == 0: - value = self._profile.point(index.row()).x + value = self._data.point(index.row()).x elif index.column() == 1: - value = self._profile.point(index.row()).y + value = self._data.point(index.row()).y elif index.column() == 2: - value = self._profile.point(index.row()).z + value = self._data.point(index.row()).z elif index.column() == 3: - value = self._profile.point(index.row()).name + value = self._data.point(index.row()).name elif index.column() == 4: - station = self._profile.get_station() + station = self._data.get_station() if station is None: return "-" else: @@ -95,25 +75,25 @@ class TableEditableModel(QAbstractTableModel): return Qt.AlignHCenter | Qt.AlignVCenter if index.column() == 2: - value = self._profile.point(index.row()).z + value = self._data.point(index.row()).z if role == Qt.ForegroundRole: - if value == self._profile.z_min(): + if value == self._data.z_min(): return QColor("red") - elif value == self._profile.z_max(): + elif value == self._data.z_max(): return QColor("blue") if role == Qt.ToolTipRole: - if value == self._profile.z_min(): + if value == self._data.z_min(): return _translate("MainWindowProfile", "La cote du fond", "Z minimale") - elif value == self._profile.z_max(): + elif value == self._data.z_max(): return _translate("MainWindowProfile", "La cote maximale", "Z maximale") if index.column() == 3: - value = self._profile.point(index.row()).name + value = self._data.point(index.row()).name if value.strip().upper() in ["RG", "RD"]: if role == Qt.FontRole: @@ -132,21 +112,6 @@ class TableEditableModel(QAbstractTableModel): return QVariant() - def headerData(self, section, orientation, role=Qt.DisplayRole): - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - return self._header[section] - elif orientation == Qt.Vertical and role == Qt.DisplayRole: - return str(section + 1) - - if role == Qt.ToolTipRole and section == 4: - return _translate( - "MainWindowProfile", - "Abscisse en travers calculée en projétant les points" - " \nsur le plan défini par les deux points nommés extrêmes " - ) - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): row = index.row() column = index.column() @@ -154,34 +119,34 @@ class TableEditableModel(QAbstractTableModel): if role == Qt.EditRole: try: if column == 0: - self._undo_stack.push( + self._undo.push( SetXCommand( - self._profile, row, - self._profile.point(row).x, + self._data, row, + self._data.point(row).x, value ) ) elif column == 1: - self._undo_stack.push( + self._undo.push( SetYCommand( - self._profile, row, - self._profile.point(row).y, + self._data, row, + self._data.point(row).y, value ) ) elif column == 2: - self._undo_stack.push( + self._undo.push( SetZCommand( - self._profile, row, - self._profile.point(row).z, + self._data, row, + self._data.point(row).z, value ) ) elif column == 3: - self._undo_stack.push( + self._undo.push( SetNameCommand( - self._profile, row, - self._profile.point(row).name, + self._data, row, + self._data.point(row).name, value ) ) @@ -213,9 +178,9 @@ class TableEditableModel(QAbstractTableModel): def insert_row(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) - self._undo_stack.push( + self._undo.push( AddCommand( - self._profile, row + self._data, row ) ) @@ -225,9 +190,9 @@ class TableEditableModel(QAbstractTableModel): def remove_rows(self, rows, parent=QModelIndex()): self.beginRemoveRows(parent, rows[0], rows[-1]) - self._undo_stack.push( + self._undo.push( DelCommand( - self._profile, rows + self._data, rows ) ) @@ -239,15 +204,15 @@ class TableEditableModel(QAbstractTableModel): reverse = (order != Qt.AscendingOrder) - self._undo_stack.push( + self._undo.push( SortCommand( - self._profile, column, reverse + self._data, column, reverse ) ) self.layoutChanged.emit() - def move_row_up(self, row, parent=QModelIndex()): + def move_up(self, row, parent=QModelIndex()): if row <= 0: return @@ -255,26 +220,26 @@ class TableEditableModel(QAbstractTableModel): self.beginMoveRows(parent, row - 1, row - 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._profile, "up", row + self._data, "up", row ) ) self.endMoveRows() self.layoutChanged.emit() - def move_row_down(self, row_to_move, parent=QModelIndex()): - if row > self._profile.number_points: + def move_down(self, row_to_move, parent=QModelIndex()): + if row > self._data.number_points: return target = row self.beginMoveRows(parent, row + 1, row + 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._profile, "down", row + self._data, "down", row ) ) @@ -282,7 +247,7 @@ class TableEditableModel(QAbstractTableModel): self.layoutChanged.emit() def paste(self, row, header, data): - if row > self._profile.number_points: + if row > self._data.number_points: return if len(data) == 0: @@ -290,12 +255,12 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( PasteCommand( - self._profile, row, + self._data, row, list( map( - lambda d: self._profile.point_from_data(header, d), + lambda d: self._data.point_from_data(header, d), data ) ) @@ -306,32 +271,9 @@ class TableEditableModel(QAbstractTableModel): self.layoutChanged.emit() def undo(self): - self._undo_stack.undo() + self._undo.undo() self.layoutChanged.emit() def redo(self): - self._undo_stack.redo() + self._undo.redo() self.layoutChanged.emit() - - -class Delegate(QStyledItemDelegate): - def __init__(self, parent=None, setModelDataEvent=None): - super(Delegate, self).__init__(parent) - self.setModelDataEvent = setModelDataEvent - - def createEditor(self, parent, option, index): - index.model().data(index, Qt.DisplayRole) - return QLineEdit(parent) - - def setEditorData(self, editor, index): - value = index.model().data(index, Qt.DisplayRole) - editor.setText(str(value)) - - def setModelData(self, editor, model, index): - model.setData(index, editor.text()) - - if not self.setModelDataEvent is None: - self.setModelDataEvent() - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) diff --git a/src/View/Geometry/Profile/Translate.py b/src/View/Geometry/Profile/Translate.py new file mode 100644 index 00000000..148426aa --- /dev/null +++ b/src/View/Geometry/Profile/Translate.py @@ -0,0 +1,36 @@ +# 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 . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate +from View.Geometry.Translate import GeometryTranslate + +_translate = QCoreApplication.translate + +class GeometryProfileTranslate(GeometryTranslate): + def __init__(self): + super(GeometryProfileTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "x": _translate("Geometry", "X (m)"), + "y": _translate("Geometry", "Y (m)"), + "z": _translate("Geometry", "Z (m)"), + "name": _translate("Geometry", "Name"), + "abs": _translate("Geometry", "Traversal abs (m)"), + } diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index eca6e7af..90fe1f44 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -31,206 +31,184 @@ from PyQt5.QtCore import ( ) from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, - QUndoStack, QShortcut, + QUndoStack, QShortcut, QTableView, QAbstractItemView, + QHeaderView, QVBoxLayout, QAction, ) from Model.Geometry.Reach import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ -from View.Tools.ASubWindow import ASubMainWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas -from View.Geometry.Profile.mainwindow_ui_profile import Ui_MainWindow from View.Geometry.Profile.Plot import Plot -from View.Geometry.Profile.Table import * +from View.Geometry.Profile.Table import GeometryProfileTableModel +from View.Geometry.Profile.Translate import GeometryProfileTranslate _translate = QCoreApplication.translate -class ProfileWindow(ASubMainWindow): - def __init__(self, profile=None, title="Profile", parent=None): - self._title = title - self.parent = parent - super(ProfileWindow, self).__init__( - name=self._title, - parent=self.parent - ) - - self.ui = Ui_MainWindow() - self.ui.setupUi(self) +class ProfileWindow(PamhyrWindow): + _pamhyr_ui = "GeometryCrossSection" + _pamhyr_name = "Geometry cross-section" + def __init__(self, profile=None, study=None, config=None, parent=None): self._profile = profile - self._model = None - self.setup_window() - self.setup_sc() - self.setup_model() - self.setup_connections() - self.plot() - - self._model.dataChanged.connect(self.update_plot) - self.fileName = None - - # self.ui.tableView.installEventFilter(self) - # self._model.dataChanged.connect(self.tableview_is_modified) - - # self.ui.btn_go_back.setEnabled(False) - # self.ui.btn_check.setEnabled(False) - # self._model.dataChanged.connect(self.set_enable_cancel_btn) - # self._model.dataChanged.connect(self.set_enable_validate_changes_btn) - # self.ui.btn_reset.setEnabled(False) - # self._model.dataChanged.connect(self.set_enable_go_back_initial_state_btn) - - def setup_window(self): - header = _translate("MainWindowProfile", "Profile") - - name = self._profile.name - if (name is None) or (name == ""): - name = _translate("MainWindowProfile", "(no name)") - - self._title = ( - header + " - " + - f"{self._profile.reach.name}" + " - " + - f"{name} ({self._profile.kp})" + name = f"{self._pamhyr_name} - {self._profile.name} {self._profile.kp}" + super(ProfileWindow, self).__init__( + title = name, + study = study, + config = config, + trad = GeometryProfileTranslate(), + parent = parent ) - self.setWindowTitle(self._title) + self.setup_table() + self.setup_plot() + self.setup_connections() - def setup_sc(self): - self._undo_stack = QUndoStack() + def setup_table(self): + table_headers = self._trad.get_dict("table_headers") - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_model(self): - self._model = TableEditableModel( - profile = self._profile, + table = self.find(QTableView, "tableView") + self._tablemodel = GeometryProfileTableModel( + table_view = table, + table_headers = table_headers, + editable_headers = ["name", "x", "y", "z"], + data = self._profile, undo = self._undo_stack ) + table.setModel(self._tablemodel) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) - self.ui.tableView.setModel(self._model) - self.ui.tableView.setItemDelegate(Delegate()) + def setup_plot(self): + self._tablemodel.blockSignals(True) - def setup_connections(self): - self.ui.btn_sort_asc_x.triggered.connect(self.sort_X_ascending) - self.ui.btn_sort_desc_x.triggered.connect(self.sort_X_descending) - self.ui.btn_sort_asc_y.triggered.connect(self.sort_Y_ascending) - self.ui.btn_sort_desc_y.triggered.connect(self.sort_Y_descending) - self.ui.btn_move_up.triggered.connect(self.move_row_up) - self.ui.btn_move_down.triggered.connect(self.move_row_down) - self.ui.btn_export.triggered.connect(self.handleSave) - self.ui.btn_add.triggered.connect(self.insert_row) - self.ui.btn_delete.triggered.connect(self.delete_row) - # self.ui.btn_copy.clicked.connect(self.copyTable) - # self.ui.btn_paste.clicked.connect(self.pasteTable) - # self.ui.btn_check.clicked.connect(self.validate_changes) - # self.ui.btn_go_back.clicked.connect(self.cancel_validate_changes) - # self.ui.btn_reset.clicked.connect(self.go_back_to_initial_state) - - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - - def plot(self): - self.ui.tableView.model().blockSignals(True) + self._canvas = MplCanvas(width=3, height=4, dpi=100) + self._canvas.setObjectName("canvas") + self._toolbar = PamhyrPlotToolbar( + self._canvas, self, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout = self.find(QVBoxLayout, "verticalLayout") + self._plot_layout.addWidget(self._toolbar) + self._plot_layout.addWidget(self._canvas) self._plot = Plot( - canvas = self.ui.canvas, + canvas = self._canvas, data = self._profile, - toolbar = None, - table = self.ui.tableView, + toolbar = self._toolbar, + table = self.find(QTableView, "tableView") ) self._plot.draw() - self.ui.tableView.model().blockSignals(False) + self._tablemodel.blockSignals(False) + + def setup_connections(self): + actions = { + "action_sort_asc": self.sort_X_ascending, + "action_sort_des": self.sort_X_descending, + "action_up": self.move_up, + "action_down": self.move_down, + "action_add": self.add, + "action_delete": self.delete, + } + + for action in actions: + self.find(QAction, action)\ + .triggered.connect(actions[action]) + + self._tablemodel.dataChanged.connect(self.update_plot) def update_plot(self): - self.ui.tableView.model().blockSignals(True) + self._tablemodel.blockSignals(True) # TODO: Do not rebuild all graph self._plot.update() - self.ui.tableView.model().blockSignals(False) + self._tablemodel.blockSignals(False) def index_selected_row(self): - rows = self.ui.tableView\ + rows = self._tablemodel\ .selectionModel()\ .selectedRows() if len(rows) == 0: return 0 - return self.ui.tableView\ + return self._tablemodel\ .selectionModel()\ .selectedRows()[0]\ .row() - def insert_row(self): - if len(self.ui.tableView.selectedIndexes()) == 0: - self._model.insert_row(self._model.rowCount()) + def add(self): + if len(self._tablemodel.selectedIndexes()) == 0: + self._tablemodel.insert_row(self._tablemodel.rowCount()) else: row = self.index_selected_row() - self._model.insert_row(row + 1) + self._tablemodel.insert_row(row + 1) self.update_plot() - def delete_row(self): + def delete(self): rows = sorted( list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in self._tablemodel.selectedIndexes()] ) ) ) if len(rows) > 0: - self._model.remove_rows(rows) + self._tablemodel.remove_rows(rows) self.update_plot() def sort_X_ascending(self): - self._model.sort('x', order=Qt.AscendingOrder) + self._tablemodel.sort('x', order=Qt.AscendingOrder) self.update_plot() def sort_X_descending(self): - self._model.sort('x', order=Qt.DescendingOrder) + self._tablemodel.sort('x', order=Qt.DescendingOrder) self.update_plot() def sort_Y_ascending(self): - self._model.sort('y', order=Qt.AscendingOrder) + self._tablemodel.sort('y', order=Qt.AscendingOrder) self.update_plot() def sort_Y_descending(self): - self._model.sort('y', order=Qt.DescendingOrder) + self._tablemodel.sort('y', order=Qt.DescendingOrder) self.update_plot() - def move_row_down(self): + def move_down(self): rows = list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in self._tablemodel.selectedIndexes()] ) ) for row in rows: - if row < self._model.rowCount() - 1: - self._model.move_row_down(row) + if row < self._tablemodel.rowCount() - 1: + self._tablemodel.move_down(row) self.update_plot() - def move_row_up(self): + def move_up(self): rows = list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in self._tablemodel.selectedIndexes()] ) ) for row in rows: if 0 < row: - self._model.move_row_up(row) + self._tablemodel.move_up(row) self.update_plot() def copy(self): - rows = self.ui.tableView\ + rows = self._tablemodel\ .selectionModel()\ .selectedRows() table = [] @@ -258,136 +236,13 @@ class ProfileWindow(ASubMainWindow): row.append(self._profile) row = self.index_selected_row() - self._model.paste(row, header, data) + self._tablemodel.paste(row, header, data) self.update_plot() def undo(self): - self._model.undo() + self._tablemodel.undo() self.update_plot() def redo(self): - self._model.redo() + self._tablemodel.redo() self.update_plot() - - def handleSave(self): - if self.fileName is None or self.fileName == '': - self.fileName, self.filters = QFileDialog.getSaveFileName( - self, filter="CSV files (*.csv)" - ) - - if self.fileName != '': - with open(self.fileName, 'w') as stream: - csvout = csv.writer(stream, delimiter='\t', quotechar=' ', - escapechar=None, - quoting=csv.QUOTE_NONNUMERIC, - lineterminator='\n') - - for row in range(self._model.rowCount(QModelIndex())): - rowdata = [] - for column in range(self._model.columnCount(QModelIndex())): - item = self._model.index( - row, column, QModelIndex() - ).data(Qt.DisplayRole) - - if item is not None: - rowdata.append(item) - - if item == 'nan': - rowdata.remove(item) - - csvout.writerow(rowdata) - - def handleOpen(self): - self.fileName, self.filterName = QFileDialog.getOpenFileName(self) - - if self.fileName != '': - with open(self.fileName, 'r') as f: - reader = csv.reader(f, delimiter='\t') - header = next(reader) - - buf = [] - for row in reader: - row[0] = QCheckBox("-") - buf.append(row) - - self._model = None - self._model = TableEditableModel(buf) - self.ui.tableView.setModel(self._model) - self.fileName = '' - - def set_enable_validate_changes_btn(self): - self.ui.btn_check.setEnabled(True) - - def set_enable_cancel_btn(self): - self.ui.btn_go_back.setEnabled(True) - - def set_enable_go_back_initial_state_btn(self): - self.ui.btn_reset.setEnabled(True) - - def delete_empty_rows(self): - if self._model.data_contains_nan(): - buttonReply = QtWidgets.QMessageBox.question( - self, - _translate("MainWindowProfile", - "Suppression les lignes incomplètes"), - _translate("MainWindowProfile", - "Supprimer les lignes des cellules" - " non renseignées ?"), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - - if buttonReply == QtWidgets.QMessageBox.Yes: - self._model.delete_empty_rows() - - if buttonReply == QtWidgets.QMessageBox.No: - pass - - def remove_duplicates_point_names(self): - counter_list = [] - list_deleted_names = [] - - for ind, name_point in enumerate(self._model.name): - if name_point not in counter_list: - counter_list.append(name_point) - elif len(name_point.strip()) > 0 and name_point in counter_list: - if name_point not in list_deleted_names: - list_deleted_names.append(name_point) - - if list_deleted_names: - self.msg_box_check_duplication_names(list_deleted_names) - - def msg_box_check_duplication_names(self, list_deleted_names): - if len(list_deleted_names) == 1: - text = _translate("MainWindowProfile", - "Le nom ''{}'' est dupliqué." - " \n\nYes : Ne garder que la première occurrence. \nNo :" - " Annuler la suppression.".format(list_deleted_names[0])) - else: - text = _translate("MainWindowProfile", - "Les noms suivants : \n{} sont dupliqués" - " \n\nYes : Ne garder que la première occurrence de " - "chaque nom. \nNo :" - " Annuler la suppression.".format(list_deleted_names)) - - buttonReply = QtWidgets.QMessageBox.question( - self, _translate("MainWindowProfile", - "Suppression des noms répétés"), - text, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - - if buttonReply == QtWidgets.QMessageBox.Yes: - self._model.remove_duplicates_names() - - def ask_quit(self): - choice = QtWidgets.QMessageBox.question( - self, - _translate("MainWindowProfile", "Quittez ?"), - _translate("MainWindowProfile", - "Etes-vous sûr de vouloir quitter ?"), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - return choice == QtWidgets.QMessageBox.Yes - - def initial_state_model(self, profile_initial_state): - return profile_initial_state diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 0ff5e0c8..89fafafa 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -46,7 +46,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableEditableModel(PamhyrTableModel): +class GeometryReachTableModel(PamhyrTableModel): def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() @@ -223,62 +223,3 @@ class TableEditableModel(PamhyrTableModel): self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - -# TODO: Delete useless delegate -class Delegate(QStyledItemDelegate): - def __init__(self, parent=None, setModelDataEvent=None): - super(Delegate, self).__init__(parent) - self.setModelDataEvent = setModelDataEvent - - def createEditor(self, parent, option, index): - index.model().data(index, Qt.DisplayRole) - return QLineEdit(parent) - - def setEditorData(self, editor, index): - value = index.model().data(index, Qt.DisplayRole) - editor.setText(str(value)) - - def setModelData(self, editor, model, index): - model.setData(index, editor.text()) - - if not self.setModelDataEvent is None: - self.setModelDataEvent() - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) - -class Delegate1(QStyledItemDelegate): - def __init__(self, owner, choices): - super().__init__(owner) - self.items = choices - - def paint(self, painter, option, index): - if isinstance(self.parent(), QAbstractItemView): - self.parent().openPersistentEditor(index) - super(Delegate1, self).paint(painter, option, index) - - def createEditor(self, parent, option, index): - editor = QComboBox(parent) - # editor.currentIndexChanged.connect(self.commit_editor) - editor.addItems(self.items) - return editor - - def setEditorData(self, editor, index): - editor.blockSignals(True) - text = index.model().data(index, Qt.DisplayRole) - try: - i = self.items.index(text) - except ValueError: - i = 0 - editor.setCurrentIndex(i) - editor.blockSignals(False) - - def setModelData(self, editor, model, index): - model.setData(index, editor.currentText(), Qt.DisplayRole) - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) - - @pyqtSlot() - def currentIndexChanged(self): - self.commitData.emit(self.sender()) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 1b334bb7..2fccde41 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -39,17 +39,16 @@ from PyQt5.QtWidgets import ( QLabel, ) -from View.Geometry.PlotXY import PlotXY -from View.Geometry.PlotKPZ import PlotKPZ -from View.Geometry.PlotAC import PlotAC - from View.Tools.PamhyrWindow import PamhyrWindow from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.Tools.Plot.PamhyrCanvas import MplCanvas -from View.Geometry.Table import * +from View.Geometry.Table import GeometryReachTableModel +from View.Geometry.PlotXY import PlotXY +from View.Geometry.PlotAC import PlotAC +from View.Geometry.PlotKPZ import PlotKPZ from View.Geometry.Translate import GeometryTranslate -# from View.Geometry.Profile.Window import ProfileWindow +from View.Geometry.Profile.Window import ProfileWindow _translate = QCoreApplication.translate @@ -87,7 +86,7 @@ class GeometryWindow(PamhyrWindow): table_headers = self._trad.get_dict("table_headers") table = self.find(QTableView, "tableView") - self._tablemodel = TableEditableModel( + self._tablemodel = GeometryReachTableModel( table_view = table, table_headers = table_headers, editable_headers = ["name", "kp"], diff --git a/src/View/ui/GeometryCrossSection.ui b/src/View/ui/GeometryCrossSection.ui index b0c12393..67e06ced 100644 --- a/src/View/ui/GeometryCrossSection.ui +++ b/src/View/ui/GeometryCrossSection.ui @@ -51,8 +51,8 @@ - - + + @@ -104,25 +104,25 @@ Mode down selected point(s) - + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png - sort + sort_asc Sort points by nearest neighbor - + ressources/gtk-sort-descending.pngressources/gtk-sort-descending.png - resort + sort_des Sort reversed points by nearest neighbor From c744797c6fa77872614a14178f7a70519b6afd26 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 3 Oct 2023 16:39:31 +0200 Subject: [PATCH 030/248] refactoring: Minor fix in geometry. --- src/View/Geometry/Profile/Window.py | 12 ++++++++---- src/View/Geometry/Window.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 90fe1f44..e4112f40 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -145,7 +145,8 @@ class ProfileWindow(PamhyrWindow): .row() def add(self): - if len(self._tablemodel.selectedIndexes()) == 0: + table = self.find(QTableView, "tableView") + if len(table.selectedIndexes()) == 0: self._tablemodel.insert_row(self._tablemodel.rowCount()) else: row = self.index_selected_row() @@ -153,10 +154,11 @@ class ProfileWindow(PamhyrWindow): self.update_plot() def delete(self): + table = self.find(QTableView, "tableView") rows = sorted( list( set( - [index.row() for index in self._tablemodel.selectedIndexes()] + [index.row() for index in table.selectedIndexes()] ) ) ) @@ -184,7 +186,8 @@ class ProfileWindow(PamhyrWindow): def move_down(self): rows = list( set( - [index.row() for index in self._tablemodel.selectedIndexes()] + [index.row() for index in + self.find(QTableView, "tableView").selectedIndexes()] ) ) @@ -197,7 +200,8 @@ class ProfileWindow(PamhyrWindow): def move_up(self): rows = list( set( - [index.row() for index in self._tablemodel.selectedIndexes()] + [index.row() for index in + self.find(QTableView, "tableView").selectedIndexes()] ) ) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 2fccde41..af27773f 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -36,7 +36,7 @@ from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, QUndoStack, QShortcut, QTableView, QHeaderView, QAction, QSlider, QPushButton, QVBoxLayout, - QLabel, + QLabel, QAbstractItemView, ) from View.Tools.PamhyrWindow import PamhyrWindow From 7a5b4779e9ca999df83f04392ab85487dfc9cb56 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 3 Oct 2023 17:11:06 +0200 Subject: [PATCH 031/248] refactoring: Minor fix in geometry. --- src/View/Geometry/Table.py | 8 ++++---- src/View/Geometry/Window.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 89fafafa..e9f6eac6 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -115,7 +115,7 @@ class GeometryReachTableModel(PamhyrTableModel): return False # @QtCore.pyqtSlot() - def insert_row(self, row, parent=QModelIndex()): + def add(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) self._undo.push( @@ -127,7 +127,7 @@ class GeometryReachTableModel(PamhyrTableModel): self.endInsertRows() self.layoutChanged.emit() - def remove_rows(self, rows, parent=QModelIndex()): + def delete(self, rows, parent=QModelIndex()): self.beginRemoveRows(parent, rows[0], rows[-1]) self._undo.push( @@ -152,7 +152,7 @@ class GeometryReachTableModel(PamhyrTableModel): self.layoutChanged.emit() - def move_row_up(self, row, parent=QModelIndex()): + def move_up(self, row, parent=QModelIndex()): if row <= 0: return @@ -169,7 +169,7 @@ class GeometryReachTableModel(PamhyrTableModel): self.endMoveRows() self.layoutChanged.emit() - def move_row_down(self, row, parent=QModelIndex()): + def move_down(self, row, parent=QModelIndex()): if row > self._data.number_profiles: return diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index af27773f..516bf66f 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -390,7 +390,7 @@ class GeometryWindow(PamhyrWindow): ) if len(rows) > 0: - self._tablemodel.remove_rows(rows) + self._tablemodel.delete(rows) self.update_plot_xy() self.select_current_profile() @@ -405,12 +405,12 @@ class GeometryWindow(PamhyrWindow): .row() def sort_ascending(self): - self._tablemodel.sort_profiles(True) + self._tablemodel.sort_profiles(False) self.select_current_profile() self.changed_slider_value() def sort_descending(self): - self._tablemodel.sort_profiles(False) + self._tablemodel.sort_profiles(True) self.select_current_profile() self.changed_slider_value() From ea3654f1f84bf85ef8f219e3fc71438f5b14c069 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 10:35:54 +0200 Subject: [PATCH 032/248] refactoring: refacto BC. --- src/View/BoundaryCondition/Edit/Plot.py | 18 ++++++++++-------- src/View/BoundaryCondition/Edit/Window.py | 7 +++---- src/View/MainWindow.py | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index a7815e60..e5e80c1e 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -21,7 +21,7 @@ import logging from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -33,16 +33,18 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, - mode = "time", toolbar=None): +class Plot(PamhyrPlot): + def __init__(self, mode = "time", data=None, + trad=None, canvas=None, toolbar=None, + parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) - self._trad = BCETranslate() self._table_headers = self._trad.get_dict("table_headers") self._mode = mode diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 13d277e6..5661d6bc 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -41,8 +41,8 @@ from PyQt5.QtWidgets import ( QHeaderView, QDoubleSpinBox, QVBoxLayout, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.BoundaryCondition.Edit.translate import BCETranslate from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand @@ -122,8 +122,6 @@ class EditBoundaryConditionWindow(PamhyrWindow): parent = parent ) - self.ui.setWindowTitle(self._title) - self.setup_table() self.setup_plot() self.setup_data() @@ -183,6 +181,7 @@ class EditBoundaryConditionWindow(PamhyrWindow): canvas = self.canvas, data = self._data, mode = self._study.time_system, + trad = self._trad, toolbar = self.toolbar, ) self.plot.draw() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 8162807e..266dc321 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -45,7 +45,7 @@ from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow -# from View.BoundaryCondition.Window import BoundaryConditionWindow +from View.BoundaryCondition.Window import BoundaryConditionWindow # from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow From 500885ec4d05d45bed7e356ba9417940c162b6d2 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 11:26:43 +0200 Subject: [PATCH 033/248] refactoring: refacto LC. --- src/View/LateralContribution/Edit/Plot.py | 22 ++++++---- src/View/LateralContribution/Edit/Window.py | 24 +++++----- .../LateralContribution/Edit/translate.py | 29 ++++++------ src/View/LateralContribution/Table.py | 11 +++-- src/View/LateralContribution/Window.py | 14 +++--- src/View/LateralContribution/translate.py | 44 ++++++++----------- src/View/MainWindow.py | 2 +- src/View/Tools/PamhyrWidget.py | 2 + 8 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src/View/LateralContribution/Edit/Plot.py b/src/View/LateralContribution/Edit/Plot.py index 5843ee56..ea1c0454 100644 --- a/src/View/LateralContribution/Edit/Plot.py +++ b/src/View/LateralContribution/Edit/Plot.py @@ -19,7 +19,7 @@ from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,15 +29,19 @@ from View.LateralContribution.Edit.translate import * _translate = QCoreApplication.translate -class Plot(APlot): - def __init__(self, canvas=None, data=None, - mode = "time", toolbar=None): +class Plot(PamhyrPlot): + def __init__(self, mode = "time", data=None, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) + self._table_headers = self._trad.get_dict("table_headers") self._mode = mode def custom_ticks(self): @@ -105,10 +109,10 @@ class Plot(APlot): # Plot label header = self.data.header self.canvas.axes.set_xlabel( - table_headers[header[0]], color='black', fontsize=10 + self._table_headers[header[0]], color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - table_headers[header[1]], color='black', fontsize=10 + self._table_headers[header[1]], color='black', fontsize=10 ) self.canvas.axes.autoscale_view(True, True, True) diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index f37c1759..55f1307f 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -36,11 +36,10 @@ from PyQt5.QtWidgets import ( QHeaderView, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.LateralContribution.translate import long_types -from View.LateralContribution.Edit.translate import table_headers, retranslate +from View.LateralContribution.Edit.translate import LCETranslate from View.LateralContribution.Edit.Table import TableModel from View.LateralContribution.Edit.Plot import Plot @@ -54,6 +53,7 @@ class EditLateralContributionWindow(PamhyrWindow): study=None, config=None, parent=None): self._data = data + trad = LCETranslate() name = self._pamhyr_name if self._data is not None: @@ -63,13 +63,14 @@ class EditLateralContributionWindow(PamhyrWindow): _translate("Edit lateral contribution", self._pamhyr_name) + f" - {study.name} " + f" - {self._data.name} ({self._data.id}) " + - f"({long_types[self._data.lctype]} - {edge_name})" + f"({trad.get_dict('long_types')[self._data.lctype]} - {edge_name})" ) super(EditLateralContributionWindow, self).__init__( title = name, study = study, config = config, + trad = trad, parent = parent ) @@ -78,17 +79,17 @@ class EditLateralContributionWindow(PamhyrWindow): self.setup_connections() def setup_table(self): - retranslate() - headers = {} - for h in self._data.header: - headers[h] = table_headers[h] - self._delegate_time = PamhyrExTimeDelegate( data = self._data, mode = self._study.time_system, parent = self ) + 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, @@ -99,6 +100,7 @@ class EditLateralContributionWindow(PamhyrWindow): }, data = self._data, undo = self._undo_stack, + trad = self._trad, opt_data = self._study.time_system ) @@ -121,6 +123,8 @@ class EditLateralContributionWindow(PamhyrWindow): data = self._data, mode = self._study.time_system, toolbar = self.toolbar, + trad = self._trad, + parent = self ) self.plot.draw() diff --git a/src/View/LateralContribution/Edit/translate.py b/src/View/LateralContribution/Edit/translate.py index f04534e0..247cdc4e 100644 --- a/src/View/LateralContribution/Edit/translate.py +++ b/src/View/LateralContribution/Edit/translate.py @@ -18,21 +18,20 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate +from View.LateralContribution.translate import LCTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("LateralContribution", "X"), - "y": _translate("LateralContribution", "Y"), - "time": _translate("LateralContribution", "Time"), - "date": _translate("LateralContribution", "Date"), - "discharge": _translate("LateralContribution", "Discharge (m³/s)"), - "z": _translate("LateralContribution", "Z (m)") -} +class LCETranslate(LCTranslate): + def __init__(self): + super(LCETranslate, self).__init__() -def retranslate(): - table_headers["x"] = _translate("LateralContribution", "X") - table_headers["y"] = _translate("LateralContribution", "Y") - table_headers["time"] = _translate("LateralContribution", "Time") - table_headers["date"] = _translate("LateralContribution", "Date") - table_headers["discharge"] = _translate("LateralContribution", "Discharge (m³/s)") - table_headers["z"] = _translate("LateralContribution", "Z (m)") + self._sub_dict["table_headers"] = { + "x": _translate("LateralContribution", "X"), + "y": _translate("LateralContribution", "Y"), + "time": _translate("LateralContribution", "Time"), + "date": _translate("LateralContribution", "Date"), + "discharge": _translate("LateralContribution", "Discharge (m³/s)"), + "z": _translate("LateralContribution", "Z (m)") + } diff --git a/src/View/LateralContribution/Table.py b/src/View/LateralContribution/Table.py index 2a3f4196..3c5f5cb7 100644 --- a/src/View/LateralContribution/Table.py +++ b/src/View/LateralContribution/Table.py @@ -46,22 +46,24 @@ from Model.LateralContribution.LateralContributionTypes import ( ) from View.Tools.PamhyrTable import PamhyrTableModel -from View.LateralContribution.translate import * +from View.LateralContribution.translate import LC_types logger = logging.getLogger() _translate = QCoreApplication.translate class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, mode="type", tab="", parent=None): + def __init__(self, data=None, mode="type", tab="", trad = None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._data = data self._mode = mode self._tab = tab + self._trad = trad def createEditor(self, parent, option, index): self.editor = QComboBox(parent) + long_types = self._trad.get_dict("long_types") if self._mode == "type": lst = list( @@ -108,6 +110,7 @@ class TableModel(PamhyrTableModel): def _setup_lst(self): self._lst = self._data.lateral_contribution self._tab = self._opt_data + self._long_types = self._trad.get_dict("long_types") def rowCount(self, parent): return self._lst.len(self._tab) @@ -123,7 +126,7 @@ class TableModel(PamhyrTableModel): return self._lst.get(self._tab, row).name elif self._headers[column] == "type": t = self._lst.get(self._tab, row).lctype - return long_types[t] + return self._long_types[t] elif self._headers[column] == "edge": n = self._lst.get(self._tab, row).edge if n is None: @@ -151,7 +154,7 @@ class TableModel(PamhyrTableModel): ) ) elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) + key = next(k for k, v in self._long_types.items() if v == value) self._undo.push( SetTypeCommand( self._lst, self._tab, row, LC_types[key] diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index 2793ee36..467f2a4b 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -53,16 +53,13 @@ from View.LateralContribution.Table import ( TableModel, ComboBoxDelegate ) -from View.Plot.MplCanvas import MplCanvas +from View.Tools.Plot.PamhyrCanvas import MplCanvas from View.Geometry.PlotXY import PlotXY from View.LateralContribution.translate import ( - long_types, table_headers, LC_types, - retranslate, + LC_types, LCTranslate, ) from View.LateralContribution.Edit.Window import EditLateralContributionWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() class LateralContributionWindow(PamhyrWindow): @@ -75,6 +72,7 @@ class LateralContributionWindow(PamhyrWindow): super(LateralContributionWindow, self).__init__( title = name, study = study, + trad = LCTranslate(), config = config, parent=parent ) @@ -86,7 +84,6 @@ class LateralContributionWindow(PamhyrWindow): self.setup_connections() def setup_table(self): - retranslate() self._table = {} for t in ["liquid", "solid", "suspenssion"]: @@ -94,19 +91,21 @@ class LateralContributionWindow(PamhyrWindow): data = self._study.river, mode = "type", tab = t, + trad = self._trad, parent=self ) self._delegate_edge = ComboBoxDelegate( data = self._study.river, mode = "edge", tab = t, + trad = self._trad, parent=self ) table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( table_view = table, - table_headers = table_headers, + table_headers = self._trad.get_dict("table_headers"), editable_headers = True, delegates = { "type": self._delegate_type, @@ -114,6 +113,7 @@ class LateralContributionWindow(PamhyrWindow): }, data = self._study.river, undo = self._undo_stack, + trad = self._trad, opt_data = t, ) table.setModel(self._table[t]) diff --git a/src/View/LateralContribution/translate.py b/src/View/LateralContribution/translate.py index cf430824..aaf44798 100644 --- a/src/View/LateralContribution/translate.py +++ b/src/View/LateralContribution/translate.py @@ -18,27 +18,14 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + from Model.LateralContribution.LateralContributionTypes import ( NotDefined, LateralContrib, Rain, Evaporation, ) _translate = QCoreApplication.translate -long_types = { - "ND": _translate("LateralContribution", "Not defined"), - "LC": _translate("LateralContribution", "Lateral contribution"), - "RA": _translate("LateralContribution", "Rain"), - "EV": _translate("LateralContribution", "Evaporation"), -} - -table_headers = { - "name": _translate("LateralContribution", "Name"), - "type": _translate("LateralContribution", "Type"), - "edge": _translate("LateralContribution", "Reach"), - "begin_kp": _translate("LateralContribution", "Begin kp (m)"), - "end_kp": _translate("LateralContribution", "End kp (m)") -} - LC_types = { "ND": NotDefined, "LC": LateralContrib, @@ -46,14 +33,21 @@ LC_types = { "EV": Evaporation, } -def retranslate(): - table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["type"] = _translate("LateralContribution", "Type") - table_headers["edge"] = _translate("LateralContribution", "Reach") - table_headers["begin_kp"] = _translate("LateralContribution", "Begin kp (m)") - table_headers["end_kp"] = _translate("LateralContribution", "End kp (m)") +class LCTranslate(PamhyrTranslate): + def __init__(self): + super(LCTranslate, self).__init__() - long_types["ND"] = _translate("LateralContribution", "Not defined") - long_types["LC"] = _translate("LateralContribution", "Lateral contribution") - long_types["RA"] = _translate("LateralContribution", "Rain") - long_types["EV"] = _translate("LateralContribution", "Evaporation") + self._sub_dict["long_types"] = { + "ND": _translate("LateralContribution", "Not defined"), + "LC": _translate("LateralContribution", "Lateral contribution"), + "RA": _translate("LateralContribution", "Rain"), + "EV": _translate("LateralContribution", "Evaporation"), + } + + self._sub_dict["table_headers"] = { + "name": _translate("LateralContribution", "Name"), + "type": _translate("LateralContribution", "Type"), + "edge": _translate("LateralContribution", "Reach"), + "begin_kp": _translate("LateralContribution", "Begin kp (m)"), + "end_kp": _translate("LateralContribution", "End kp (m)") + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 266dc321..5ee7a382 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -46,7 +46,7 @@ from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow -# from View.LateralContribution.Window import LateralContributionWindow +from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow # from View.Frictions.Window import FrictionsWindow diff --git a/src/View/Tools/PamhyrWidget.py b/src/View/Tools/PamhyrWidget.py index 8c85b1c1..f906beed 100644 --- a/src/View/Tools/PamhyrWidget.py +++ b/src/View/Tools/PamhyrWidget.py @@ -18,6 +18,8 @@ import logging +from datetime import datetime, timedelta + from PyQt5.QtCore import ( QModelIndex, QRect, QTime, QDateTime, ) From b430cbb62bf63254234dd5d9bb033ba0c57be41e Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 13:51:01 +0200 Subject: [PATCH 034/248] refactoring: refacto IC. --- src/View/InitialConditions/DialogDischarge.py | 2 +- src/View/InitialConditions/DialogHeight.py | 2 +- src/View/InitialConditions/PlotDKP.py | 15 ++--- src/View/InitialConditions/PlotDischarge.py | 21 ++----- src/View/InitialConditions/Table.py | 56 ++++++------------- src/View/InitialConditions/Window.py | 41 +++++++------- src/View/InitialConditions/translate.py | 35 ++++++------ src/View/MainWindow.py | 2 +- src/View/Tools/PamhyrTranslate.py | 2 + 9 files changed, 70 insertions(+), 106 deletions(-) diff --git a/src/View/InitialConditions/DialogDischarge.py b/src/View/InitialConditions/DialogDischarge.py index 7bbe8abd..7be75baf 100644 --- a/src/View/InitialConditions/DialogDischarge.py +++ b/src/View/InitialConditions/DialogDischarge.py @@ -37,7 +37,7 @@ class DischargeDialog(PamhyrDialog): def __init__(self, title="Discharge", parent=None): super(DischargeDialog, self).__init__( - title = _pamhyr_name, + title = self._pamhyr_name, options = [], parent = parent ) diff --git a/src/View/InitialConditions/DialogHeight.py b/src/View/InitialConditions/DialogHeight.py index 9ccadbd6..8a2a3c12 100644 --- a/src/View/InitialConditions/DialogHeight.py +++ b/src/View/InitialConditions/DialogHeight.py @@ -37,7 +37,7 @@ class HeightDialog(PamhyrDialog): def __init__(self, parent=None): super(HeightDialog, self).__init__( - title = name, + title = self._pamhyr_name, options = [], parent = parent ) diff --git a/src/View/InitialConditions/PlotDKP.py b/src/View/InitialConditions/PlotDKP.py index 58a83219..d604179a 100644 --- a/src/View/InitialConditions/PlotDKP.py +++ b/src/View/InitialConditions/PlotDKP.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,7 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotDKP(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotDKP, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) - +class PlotDKP(PamhyrPlot): @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -42,11 +35,11 @@ class PlotDKP(APlot): return self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Elevation (m)"), + self._trad["elevation"], color='green', fontsize=11 ) self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "KP (m)"), + self._trad["kp"], color='green', fontsize=11 ) diff --git a/src/View/InitialConditions/PlotDischarge.py b/src/View/InitialConditions/PlotDischarge.py index 1fe05def..1ad2a266 100644 --- a/src/View/InitialConditions/PlotDischarge.py +++ b/src/View/InitialConditions/PlotDischarge.py @@ -17,22 +17,9 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot - -from PyQt5.QtCore import ( - QCoreApplication -) - -_translate = QCoreApplication.translate - -class PlotDischarge(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotDischarge, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) +from View.Tools.PamhyrPlot import PamhyrPlot +class PlotDischarge(PamhyrPlot): @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -42,11 +29,11 @@ class PlotDischarge(APlot): return self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Discharge (m^3/s)"), + self._trad["discharge"], color='green', fontsize=11 ) self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "KP (m)"), + self._trad["kp"], color='green', fontsize=11 ) diff --git a/src/View/InitialConditions/Table.py b/src/View/InitialConditions/Table.py index 5e704ba9..f7ad1020 100644 --- a/src/View/InitialConditions/Table.py +++ b/src/View/InitialConditions/Table.py @@ -33,14 +33,14 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.InitialConditions.UndoCommand import ( SetCommand, AddCommand, DelCommand, SortCommand, MoveCommand, PasteCommand, DuplicateCommand, GenerateCommand, ) -from View.InitialConditions.translate import * - logger = logging.getLogger() _translate = QCoreApplication.translate @@ -87,29 +87,13 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, river=None, reach=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._river = river +class InitialConditionTableModel(PamhyrTableModel): + def __init__(self, reach=None, **kwargs): self._reach = reach - self._undo = undo - self._ics = self._river.initial_conditions.get(reach) + super(InitialConditionTableModel, self).__init__(**kwargs) - def flags(self, index): - column = index.column() - - options = Qt.ItemIsSelectable - if self._headers[column] != "speed": - options |= Qt.ItemIsEnabled | Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._ics) - - def columnCount(self, parent): - return len(self._headers) + def _setup_lst(self): + self._lst = self._data.river.initial_conditions.get(self._reach) def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -119,16 +103,10 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] not in ["name", "comment"]: - v = self._ics.get(row)[self._headers[column]] + v = self._lst.get(row)[self._headers[column]] return f"{v:.4f}" else: - return self._ics.get(row)[self._headers[column]] - - return QVariant() - - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[section]] + return self._lst.get(row)[self._headers[column]] return QVariant() @@ -143,7 +121,7 @@ class TableModel(QAbstractTableModel): if self._headers[column] is not None: self._undo.push( SetCommand( - self._ics, row, self._headers[column], value + self._lst, row, self._headers[column], value ) ) except Exception as e: @@ -158,7 +136,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._ics, row + self._lst, row ) ) @@ -170,7 +148,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._ics, rows + self._lst, rows ) ) @@ -182,7 +160,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SortCommand( - self._ics, False + self._lst, False ) ) @@ -199,7 +177,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._ics, "up", row + self._lst, "up", row ) ) @@ -207,7 +185,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._ics): + if row > len(self._lst): return target = row @@ -216,7 +194,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._ics, "down", row + self._lst, "down", row ) ) @@ -234,7 +212,7 @@ class TableModel(QAbstractTableModel): def generate(self, generator, param): self._undo.push( GenerateCommand( - self._ics, generator, param + self._lst, generator, param ) ) self.layoutChanged.emit() diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 3f5ee1d8..90cf8e08 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -45,14 +45,14 @@ from View.InitialConditions.UndoCommand import ( DuplicateCommand, ) -from View.InitialConditions.Table import TableModel, ComboBoxDelegate +from View.InitialConditions.Table import InitialConditionTableModel, ComboBoxDelegate -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.InitialConditions.PlotDKP import PlotDKP from View.InitialConditions.PlotDischarge import PlotDischarge -from View.InitialConditions.translate import table_headers, retranslate +from View.InitialConditions.translate import ICTranslate from View.InitialConditions.DialogHeight import HeightDialog from View.InitialConditions.DialogDischarge import DischargeDialog @@ -80,43 +80,42 @@ class InitialConditionsWindow(PamhyrWindow): title = name, study = study, config = config, + trad = ICTranslate(), parent = parent ) self._ics = study.river.initial_conditions.get(self._reach) self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() self.ui.setWindowTitle(self._title) def setup_table(self): - retranslate() - table = self.find(QTableView, f"tableView") - self._table = TableModel( - river = self._study.river, - reach = self._reach, - undo = self._undo_stack, - ) - table.setModel(self._table) - self._delegate_kp = ComboBoxDelegate( reach = self._reach, - parent=self + parent = self ) - table.setItemDelegateForColumn( - list(table_headers).index("kp"), - self._delegate_kp + self._table = InitialConditionTableModel( + reach = self._reach, + table_view = table, + table_headers = self._trad.get_dict("table_headers"), + editable_headers = ["kp", "discharge", "elevation", "height"], + delegates = {"kp": self._delegate_kp}, + data = self._study, + undo = self._undo_stack, + trad = self._trad ) + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): + def setup_plot(self): self.canvas_1 = MplCanvas(width=5, height=4, dpi=100) self.canvas_1.setObjectName("canvas_1") self.toolbar_1 = PamhyrPlotToolbar( @@ -129,7 +128,9 @@ class InitialConditionsWindow(PamhyrWindow): self.plot_1 = PlotDKP( canvas = self.canvas_1, data = self._ics, + trad = self._trad, toolbar = self.toolbar_1, + parent = self ) self.plot_1.draw() @@ -145,7 +146,9 @@ class InitialConditionsWindow(PamhyrWindow): self.plot_2 = PlotDischarge( canvas = self.canvas_2, data = self._ics, + trad = self._trad, toolbar = self.toolbar_2, + parent = self ) self.plot_2.draw() diff --git a/src/View/InitialConditions/translate.py b/src/View/InitialConditions/translate.py index 54f194e9..f65306cb 100644 --- a/src/View/InitialConditions/translate.py +++ b/src/View/InitialConditions/translate.py @@ -18,23 +18,24 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - # "name": _translate("LateralContribution", "Name"), - "kp": _translate("LateralContribution", "KP (m)"), - # "speed": _translate("LateralContribution", "Speed (m/s)"), - "discharge": _translate("LateralContribution", "Discharge (m³/s)"), - "elevation": _translate("LateralContribution", "Elevation (m)"), - "height": _translate("LateralContribution", "Height (m)"), - # "comment": _translate("LateralContribution", "Comment"), -} +class ICTranslate(PamhyrTranslate): + def __init__(self): + super(ICTranslate, self).__init__() -def retranslate(): - # table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["kp"] = _translate("LateralContribution", "KP (m)") - # table_headers["speed"] = _translate("LateralContribution", "Speed (m/s)") - table_headers["discharge"] = _translate("LateralContribution", "Discharge (m³/s)") - table_headers["elevation"] = _translate("LateralContribution", "Elevation (m)") - table_headers["height"] = _translate("LateralContribution", "Height (m)") - # table_headers["comment"] = _translate("LateralContribution", "Comment") + self._dict["elevation"] = _translate("InitialCondition", "Elevation (m)") + self._dict["discharge"] = _translate("InitialCondition", "Discharge (m³/s)") + self._dict["kp"] = _translate("InitialCondition", "KP (m)") + + self._sub_dict["table_headers"] = { + # "name": _translate("InitialCondition", "Name"), + "kp": _translate("InitialCondition", "KP (m)"), + # "speed": _translate("InitialCondition", "Speed (m/s)"), + "discharge": _translate("InitialCondition", "Discharge (m³/s)"), + "elevation": _translate("InitialCondition", "Elevation (m)"), + "height": _translate("InitialCondition", "Height (m)"), + # "comment": _translate("InitialCondition", "Comment"), + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 5ee7a382..c3ec403c 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -47,7 +47,7 @@ from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow -# from View.InitialConditions.Window import InitialConditionsWindow +from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow # from View.Frictions.Window import FrictionsWindow # from View.SedimentLayers.Window import SedimentLayersWindow diff --git a/src/View/Tools/PamhyrTranslate.py b/src/View/Tools/PamhyrTranslate.py index 83c8da64..67befa23 100644 --- a/src/View/Tools/PamhyrTranslate.py +++ b/src/View/Tools/PamhyrTranslate.py @@ -28,6 +28,8 @@ class PamhyrTranslate(object): # Module sub dictionnary self._sub_dict = {} + self._dict["pamhyr"] = _translate("Pamhyr", "Pamhyr2") + def __getitem__(self, index): if index not in self._dict: return None From 4ce597a2a07031ef9135e48bbe57fae34b8fac69 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 14:34:07 +0200 Subject: [PATCH 035/248] refactoring: refacto frictions & stricklers. --- src/View/Frictions/PlotStricklers.py | 11 +---- src/View/Frictions/Table.py | 64 ++++++++++------------------ src/View/Frictions/Window.py | 38 ++++++++--------- src/View/Frictions/translate.py | 28 ++++++------ src/View/MainWindow.py | 4 +- src/View/Stricklers/Window.py | 6 +-- src/View/Stricklers/translate.py | 25 +++++------ 7 files changed, 72 insertions(+), 104 deletions(-) diff --git a/src/View/Frictions/PlotStricklers.py b/src/View/Frictions/PlotStricklers.py index 71ff6e6f..c6b58dcd 100644 --- a/src/View/Frictions/PlotStricklers.py +++ b/src/View/Frictions/PlotStricklers.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer, flatten -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,7 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotStricklers(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotStricklers, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) - +class PlotStricklers(PamhyrPlot): def draw_frictions(self, frictions, color="r"): lst = frictions lst.sort(key = lambda s: s.begin_kp) diff --git a/src/View/Frictions/Table.py b/src/View/Frictions/Table.py index ef9dc32f..f4ad4f8e 100644 --- a/src/View/Frictions/Table.py +++ b/src/View/Frictions/Table.py @@ -41,6 +41,8 @@ from View.Frictions.UndoCommand import ( MoveCommand, PasteCommand, DuplicateCommand, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.Frictions.translate import * logger = logging.getLogger() @@ -93,26 +95,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, data=None, study=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._data = data - self._study = study - self._undo = undo - self._frictions = self._data.frictions - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._frictions) - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data.frictions + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -122,30 +108,24 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._frictions.get(row).name + return self._lst.get(row).name elif self._headers[column] == "begin_kp": - return self._frictions.get(row).begin_kp + return self._lst.get(row).begin_kp elif self._headers[column] == "end_kp": - return self._frictions.get(row).end_kp + return self._lst.get(row).end_kp elif self._headers[column] == "begin_strickler": - value = self._frictions.get(row).begin_strickler + value = self._lst.get(row).begin_strickler if value == None: return _translate("Frictions", "Not defined") return str(value) elif self._headers[column] == "end_strickler": - value = self._frictions.get(row).end_strickler + value = self._lst.get(row).end_strickler if value == None: return _translate("Frictions", "Not defined") return str(value) return QVariant() - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -157,31 +137,31 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "begin_kp": self._undo.push( SetBeginCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "end_kp": self._undo.push( SetEndCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "begin_strickler": self._undo.push( SetBeginStricklerCommand( - self._frictions, row, self._study.river.strickler(value) + self._lst, row, self._study.river.strickler(value) ) ) elif self._headers[column] == "end_strickler": self._undo.push( SetEndStricklerCommand( - self._frictions, row, self._study.river.strickler(value) + self._lst, row, self._study.river.strickler(value) ) ) except Exception as e: @@ -196,7 +176,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._frictions, row, self._data + self._lst, row, self._data ) ) @@ -208,7 +188,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._frictions, rows + self._lst, rows ) ) @@ -220,7 +200,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SortCommand( - self._frictions, False + self._lst, False ) ) @@ -237,7 +217,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._frictions, "up", row + self._lst, "up", row ) ) @@ -245,7 +225,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._frictions): + if row > len(self._lst): return target = row @@ -254,7 +234,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._frictions, "down", row + self._lst, "down", row ) ) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 2a547528..74d96b63 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -47,15 +47,13 @@ from View.Frictions.Table import ( TableModel, ComboBoxDelegate ) -from View.Plot.MplCanvas import MplCanvas +from View.Tools.Plot.PamhyrCanvas import MplCanvas from View.Geometry.PlotKPZ import PlotKPZ from View.Frictions.PlotStricklers import PlotStricklers -from View.Frictions.translate import table_headers, retranslate +from View.Frictions.translate import FrictionsTranslate from View.Stricklers.Window import StricklersWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() class FrictionsWindow(PamhyrWindow): @@ -80,6 +78,7 @@ class FrictionsWindow(PamhyrWindow): title = name, study = study, config = config, + trad = FrictionsTranslate(), parent = parent ) @@ -89,17 +88,8 @@ class FrictionsWindow(PamhyrWindow): def setup_table(self): - retranslate() self._table = {} - table = self.find(QTableView, f"tableView") - self._table = TableModel( - data = self._reach, - study = self._study, - undo = self._undo_stack, - ) - table.setModel(self._table) - self._delegate_stricklers = ComboBoxDelegate( data = self._reach, study = self._study, @@ -107,13 +97,23 @@ class FrictionsWindow(PamhyrWindow): parent=self ) - table.setItemDelegateForColumn( - 3, self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view = table, + table_headers = self._trad.get_dict("table_headers"), + editable_headers = [ + "name", "begin_kp", "end_kp", + "begin_strickler", "end_strickler" + ], + delegates = { + "begin_strickler": self._delegate_stricklers, + "end_strickler": self._delegate_stricklers, + }, + data = self._reach, + undo = self._undo_stack, + opt_data = self._study ) - table.setItemDelegateForColumn( - 4, self._delegate_stricklers - ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) diff --git a/src/View/Frictions/translate.py b/src/View/Frictions/translate.py index 68bebb96..1014d8c1 100644 --- a/src/View/Frictions/translate.py +++ b/src/View/Frictions/translate.py @@ -18,21 +18,19 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("Frictions", "Name"), - # "edge": _translate("Frictions", "Reach"), - "begin_kp": _translate("Frictions", "Begin kp (m)"), - "end_kp": _translate("Frictions", "End kp (m)"), - "begin_strickler": _translate("Frictions", "Begin strickler"), - "end_strickler": _translate("Frictions", "End strickler"), -} +class FrictionsTranslate(PamhyrTranslate): + def __init__(self): + super(FrictionsTranslate, self).__init__() -def retranslate(): - table_headers["name"] = _translate("Frictions", "Name") - # table_headers["edge"] = _translate("Frictions", "Reach") - table_headers["begin_kp"] = _translate("Frictions", "Begin kp (m)") - table_headers["end_kp"] = _translate("Frictions", "End kp (m)") - table_headers["begin_strickler"] = _translate("Frictions", "Begin strickler") - table_headers["end_strickler"] = _translate("Frictions", "End strickler") + self._sub_dict["table_headers"] = { + "name": _translate("Frictions", "Name"), + # "edge": _translate("Frictions", "Reach"), + "begin_kp": _translate("Frictions", "Begin kp (m)"), + "end_kp": _translate("Frictions", "End kp (m)"), + "begin_strickler": _translate("Frictions", "Begin strickler"), + "end_strickler": _translate("Frictions", "End strickler"), + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index c3ec403c..9df2d45e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -48,8 +48,8 @@ from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow -# from View.Stricklers.Window import StricklersWindow -# from View.Frictions.Window import FrictionsWindow +from View.Stricklers.Window import StricklersWindow +from View.Frictions.Window import FrictionsWindow # from View.SedimentLayers.Window import SedimentLayersWindow # from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index d9128d00..792ed4e3 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -43,7 +43,7 @@ from Model.Stricklers.Stricklers import Stricklers from View.Stricklers.UndoCommand import PasteCommand from View.Stricklers.Table import TableModel -from View.Stricklers.translate import table_headers, retranslate +from View.Stricklers.translate import StricklersTranslate _translate = QCoreApplication.translate @@ -60,6 +60,7 @@ class StricklersWindow(PamhyrWindow): title = name, study = study, config = config, + trad = StricklersTranslate(), parent = parent ) @@ -67,7 +68,6 @@ class StricklersWindow(PamhyrWindow): self.setup_connections() def setup_table(self): - retranslate() self._table = {} for t in ["app", "study"]: @@ -79,7 +79,7 @@ class StricklersWindow(PamhyrWindow): self._table[t] = TableModel( table_view = table, - table_headers = table_headers, + table_headers = self._trad.get_dict("table_headers"), editable_headers = ["name", "comment", "minor", "medium"], data = data, undo = self._undo_stack, diff --git a/src/View/Stricklers/translate.py b/src/View/Stricklers/translate.py index 9102ac6d..489094cf 100644 --- a/src/View/Stricklers/translate.py +++ b/src/View/Stricklers/translate.py @@ -18,21 +18,18 @@ from PyQt5.QtCore import QCoreApplication -from Model.LateralContribution.LateralContributionTypes import ( - NotDefined, LateralContrib, Rain, Evaporation, -) +from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate -table_headers = { - "name": _translate("LateralContribution", "Name"), - "minor": _translate("LateralContribution", "Minor bed"), - "medium": _translate("LateralContribution", "Medium bed"), - "comment": _translate("LateralContribution", "Comment"), -} -def retranslate(): - table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["minor"] = _translate("LateralContribution", "Minor bed") - table_headers["medium"] = _translate("LateralContribution", "Medium bed") - table_headers["comment"] = _translate("LateralContribution", "Comment") +class StricklersTranslate(PamhyrTranslate): + def __init__(self): + super(StricklersTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("LateralContribution", "Name"), + "minor": _translate("LateralContribution", "Minor bed"), + "medium": _translate("LateralContribution", "Medium bed"), + "comment": _translate("LateralContribution", "Comment"), + } From 1d45ccb8b80e091be5454614db36b60f761a54ab Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 15:19:27 +0200 Subject: [PATCH 036/248] refactoring: refacto SL list & edit. --- src/View/MainWindow.py | 2 +- src/View/SedimentLayers/Edit/Plot.py | 28 ++++------ src/View/SedimentLayers/Edit/Table.py | 64 +++++++---------------- src/View/SedimentLayers/Edit/Window.py | 22 ++++---- src/View/SedimentLayers/Edit/translate.py | 30 +++++------ src/View/SedimentLayers/Table.py | 50 +++++------------- src/View/SedimentLayers/Window.py | 28 +++++----- src/View/SedimentLayers/translate.py | 16 +++--- 8 files changed, 91 insertions(+), 149 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 9df2d45e..20ab32e2 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -50,7 +50,7 @@ from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow -# from View.SedimentLayers.Window import SedimentLayersWindow +from View.SedimentLayers.Window import SedimentLayersWindow # from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow # from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index e48ec5eb..a6fa2185 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -5,23 +5,19 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot - -from PyQt5.QtCore import ( - QCoreApplication -) - -_translate = QCoreApplication.translate +from View.Tools.PamhyrPlot import PamhyrPlot logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): +class Plot(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + display_current=True, parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self._display_current = display_current @@ -34,13 +30,9 @@ class Plot(APlot): self.canvas.axes.cla() self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) - # self.canvas.axes.set_xlabel( - # _translate("MainWindow_reach", "X (m)"), - # color='green', fontsize=12 - # ) self.canvas.axes.axes.get_xaxis().set_visible(False) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Height (m)"), + self._trad["height"], color='green', fontsize=12 ) diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index e1a41cb2..ea7cd934 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -15,32 +15,14 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SedimentLayers.Edit.UndoCommand import * -from View.SedimentLayers.Edit.translate import * _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, study=None, sl=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._sl = sl - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._sl) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() @@ -49,23 +31,17 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._sl.get(row).name + return self._data.get(row).name elif self._headers[column] == "type": - return self._sl.get(row).type + return self._data.get(row).type elif self._headers[column] == "height": - return self._sl.get(row).height + return self._data.get(row).height elif self._headers[column] == "d50": - return self._sl.get(row).d50 + return self._data.get(row).d50 elif self._headers[column] == "sigma": - return self._sl.get(row).sigma + return self._data.get(row).sigma elif self._headers[column] == "critical_constraint": - return self._sl.get(row).critical_constraint - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.get(row).critical_constraint return QVariant() @@ -79,37 +55,37 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "type": self._undo.push( SetTypeCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "height": self._undo.push( SetHeightCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "d50": self._undo.push( SetD50Command( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "sigma": self._undo.push( SetSigmaCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "critical_constraint": self._undo.push( SetCriticalConstraintCommand( - self._sl, row, value + self._data, row, value ) ) @@ -121,7 +97,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sl, row + self._data, row ) ) @@ -133,7 +109,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._sl, rows + self._data, rows ) ) @@ -150,7 +126,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._sl, "up", row + self._data, "up", row ) ) @@ -158,7 +134,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, row, parent=QModelIndex()): - if row + 1 >= len(self._sl): + if row + 1 >= len(self._data): return target = row @@ -167,7 +143,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._sl, "down", row + self._data, "down", row ) ) diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index ebe3aece..e5835c97 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -39,13 +39,13 @@ from PyQt5.QtWidgets import ( QComboBox, QVBoxLayout, QHeaderView, QTabWidget, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.SedimentLayers.Edit.UndoCommand import * from View.SedimentLayers.Edit.Table import * from View.SedimentLayers.Edit.Plot import Plot -from View.SedimentLayers.Edit.translate import table_headers, retranslate +from View.SedimentLayers.Edit.translate import SedimentEditTranslate _translate = QCoreApplication.translate @@ -71,7 +71,8 @@ class EditSedimentLayersWindow(PamhyrWindow): title = name, study = study, config = config, - parent=parent + trad = SedimentEditTranslate(), + parent = parent ) self.setup_table() @@ -79,17 +80,17 @@ class EditSedimentLayersWindow(PamhyrWindow): self.setup_connections() def setup_table(self): - retranslate() - self._table = {} - + table_headers = self._trad.get_dict("table_headers") table = self.find(QTableView, f"tableView") self._table = TableModel( - study = self._study, - sl = self._sl, + table_view = table, + table_headers = table_headers, + editable_headers = list(table_headers), + data = self._sl, + trad = self._trad, undo = self._undo_stack, ) table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) @@ -110,6 +111,7 @@ class EditSedimentLayersWindow(PamhyrWindow): canvas = self.canvas, data = self._sl, toolbar = self.toolbar, + trad = self._trad, display_current = False ) self.plot.draw() diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py index a6b77897..658d4f23 100644 --- a/src/View/SedimentLayers/Edit/translate.py +++ b/src/View/SedimentLayers/Edit/translate.py @@ -2,21 +2,21 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.translate import SedimentTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - # "type": _translate("SedimentLayers", "Type"), - "height": _translate("SedimentLayers", "Height"), - "d50": _translate("SedimentLayers", "D50"), - "sigma": _translate("SedimentLayers", "Sigma"), - "critical_constraint": _translate("SedimentLayers", "Critical constraint"), -} +class SedimentEditTranslate(SedimentTranslate): + def __init__(self): + super(SedimentEditTranslate, self).__init__() -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - # table_headers["type"] = _translate("SedimentLayers", "Type") - table_headers["height"] = _translate("SedimentLayers", "Height") - table_headers["d50"] = _translate("SedimentLayers", "D50") - table_headers["sigma"] = _translate("SedimentLayers", "Sigma") - table_headers["critical_constraint"] = _translate("SedimentLayers", "Critical constraint") + self._dict["height"] = _translate("SedimentLayers", "Height (m)"), + + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + # "type": _translate("SedimentLayers", "Type"), + "height": _translate("SedimentLayers", "Height"), + "d50": _translate("SedimentLayers", "D50"), + "sigma": _translate("SedimentLayers", "Sigma"), + "critical_constraint": _translate("SedimentLayers", "Critical constraint"), + } diff --git a/src/View/SedimentLayers/Table.py b/src/View/SedimentLayers/Table.py index f375c318..2b7a00f6 100644 --- a/src/View/SedimentLayers/Table.py +++ b/src/View/SedimentLayers/Table.py @@ -15,32 +15,12 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SedimentLayers.UndoCommand import * -from View.SedimentLayers.translate import * - -_translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, study=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._sl = self._study.river.sediment_layers - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._sl) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() @@ -49,15 +29,9 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._sl.get(row).name + return self._data.get(row).name elif self._headers[column] == "comment": - return self._sl.get(row).comment - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.get(row).comment return QVariant() @@ -71,13 +45,13 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "comment": self._undo.push( SetCommentCommand( - self._sl, row, value + self._data, row, value ) ) @@ -89,7 +63,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sl, row + self._data, row ) ) @@ -101,7 +75,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._sl, rows + self._data, rows ) ) @@ -118,7 +92,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._sl, "up", row + self._data, "up", row ) ) @@ -126,7 +100,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._sl): + if row > len(self._data): return target = row @@ -135,7 +109,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._sl, "down", row + self._data, "down", row ) ) diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index c4010745..3c394de0 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -44,13 +44,11 @@ from View.SedimentLayers.Table import * from View.SedimentLayers.Edit.Plot import Plot -from View.Plot.MplCanvas import MplCanvas -from View.SedimentLayers.translate import table_headers, retranslate +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.SedimentLayers.translate import SedimentTranslate from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() class SedimentLayersWindow(PamhyrWindow): @@ -68,34 +66,31 @@ class SedimentLayersWindow(PamhyrWindow): title = name, study = study, config = config, + trad = SedimentTranslate(), parent=parent ) self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() - def setup_title(self, title): - self._title = ( - title + " - " + self._study.name - ) - def setup_table(self): - retranslate() - table = self.find(QTableView, f"tableView") self._table = TableModel( - study = self._study, + table_view = table, + table_headers = self._trad.get_dict("table_headers"), + editable_headers = ["name", "comment"], + data = self._study.river.sediment_layers, + trad = self._trad, undo = self._undo_stack, ) table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): - self.canvas = MplCanvas(width=5, height=4, dpi=100) + def setup_plot(self): + self.canvas = MplCanvas(width=2, height=4, dpi=100) self.canvas.setObjectName("canvas") self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) @@ -122,6 +117,7 @@ class SedimentLayersWindow(PamhyrWindow): self.plot = Plot( canvas = self.canvas, data = self._sediment_layers.get(rows[0]), + trad = self._trad, toolbar = None, display_current = False ) diff --git a/src/View/SedimentLayers/translate.py b/src/View/SedimentLayers/translate.py index 29053e17..42d59f92 100644 --- a/src/View/SedimentLayers/translate.py +++ b/src/View/SedimentLayers/translate.py @@ -2,13 +2,15 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - "comment": _translate("SedimentLayers", "Comment"), -} +class SedimentTranslate(PamhyrTranslate): + def __init__(self): + super(SedimentTranslate, self).__init__() -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["comment"] = _translate("SedimentLayers", "Comment") + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + "comment": _translate("SedimentLayers", "Comment"), + } From 63d6c114c0f36f6a41fee6f70b7407e4f23a9c68 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 16:16:37 +0200 Subject: [PATCH 037/248] refactoring: refacto SL reach and profile window. --- src/View/MainWindow.py | 2 +- src/View/SedimentLayers/Reach/Plot.py | 21 ++++---- src/View/SedimentLayers/Reach/Profile/Plot.py | 17 ++++--- .../SedimentLayers/Reach/Profile/Table.py | 46 +++++------------ .../SedimentLayers/Reach/Profile/Window.py | 36 ++++++------- .../SedimentLayers/Reach/Profile/translate.py | 25 +++++----- src/View/SedimentLayers/Reach/Table.py | 50 ++++++------------- src/View/SedimentLayers/Reach/Window.py | 40 ++++++++------- src/View/SedimentLayers/Reach/translate.py | 23 +++++---- 9 files changed, 114 insertions(+), 146 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 20ab32e2..9fee3a76 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -51,7 +51,7 @@ from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow -# from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow +from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow # from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow # from View.CheckList.Window import CheckListWindow diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py index a312a8ce..069a09d7 100644 --- a/src/View/SedimentLayers/Reach/Plot.py +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,13 +15,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): +class Plot(PamhyrPlot): + def __init__(self, data=None, display_current=True, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self._display_current = display_current @@ -41,11 +44,11 @@ class Plot(APlot): return self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "Kp (m)"), + self._trad["kp"], color='green', fontsize=12 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Height (m)"), + self._trad["height"], color='green', fontsize=12 ) diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py index 50b4bba2..01b2fe24 100644 --- a/src/View/SedimentLayers/Reach/Profile/Plot.py +++ b/src/View/SedimentLayers/Reach/Profile/Plot.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,13 +15,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): +class Plot(PamhyrPlot): + def __init__(self, data=None, display_current=True, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self._display_current = display_current diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py index 102bec32..3550940b 100644 --- a/src/View/SedimentLayers/Reach/Profile/Table.py +++ b/src/View/SedimentLayers/Reach/Profile/Table.py @@ -20,6 +20,8 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.Reach.Profile.UndoCommand import * from View.SedimentLayers.Reach.Profile.translate import * +from View.Tools.PamhyrTable import PamhyrTableModel + _translate = QCoreApplication.translate logger = logging.getLogger() @@ -67,28 +69,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, study=None, profile=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._profile = profile - - def flags(self, index): - column = index.column() - - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if self._headers[column] == "sl": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return self._profile.number_points - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -98,25 +82,19 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._profile.point(row).name + return self._data.point(row).name elif self._headers[column] == "sl": - value = self._profile.point(row).sl + value = self._data.point(row).sl if value == None: text = _translate("SedimentLayers", "Not defined") return text return str(value) elif self._headers[column] == "x": - return self._profile.point(row).x + return self._data.point(row).x elif self._headers[column] == "y": - return self._profile.point(row).y + return self._data.point(row).y elif self._headers[column] == "z": - return self._profile.point(row).z - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.point(row).z return QVariant() @@ -139,7 +117,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SetSLCommand( - self._profile, row, new + self._data, row, new ) ) diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index f424e06a..e2c2464b 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -27,10 +27,10 @@ from View.SedimentLayers.Reach.Profile.UndoCommand import * from View.SedimentLayers.Reach.Profile.Table import * from View.SedimentLayers.Reach.Profile.Plot import Plot -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.SedimentLayers.Reach.Profile.translate import table_headers, retranslate +from View.SedimentLayers.Reach.Profile.translate import SedimentProfileTranslate from View.SedimentLayers.Window import SedimentLayersWindow _translate = QCoreApplication.translate @@ -52,7 +52,8 @@ class ProfileSedimentLayersWindow(PamhyrWindow): title = name, study = study, config = config, - parent=parent + trad = SedimentProfileTranslate(), + parent = parent ) self.setup_table() @@ -80,26 +81,25 @@ class ProfileSedimentLayersWindow(PamhyrWindow): ) def setup_table(self): - retranslate() + table_headers = self._trad.get_dict("table_headers") - table = self.find(QTableView, f"tableView") - self._table = TableModel( - study = self._study, - profile = self._profile, - undo = self._undo_stack, - ) - table.setModel(self._table) - - self._delegate_stricklers = ComboBoxDelegate( + self._delegate_sl = ComboBoxDelegate( study = self._study, parent=self ) - table.setItemDelegateForColumn( - list(table_headers).index("sl"), - self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view = table, + data = self._profile, + opt_data = self._study, + table_headers = table_headers, + editable_headers = ["sl"], + delegates = {"sl": self._delegate_sl}, + trad = self._trad, + undo = self._undo_stack, ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) diff --git a/src/View/SedimentLayers/Reach/Profile/translate.py b/src/View/SedimentLayers/Reach/Profile/translate.py index da256cbe..b1c12405 100644 --- a/src/View/SedimentLayers/Reach/Profile/translate.py +++ b/src/View/SedimentLayers/Reach/Profile/translate.py @@ -2,19 +2,18 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.Reach.translate import SedimentReachTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("SedimentLayers", "X (m)"), - "y": _translate("SedimentLayers", "Y (m)"), - "z": _translate("SedimentLayers", "Z (m)"), - "name": _translate("SedimentLayers", "Name"), - "sl": _translate("SedimentLayers", "Sediment layers"), -} +class SedimentProfileTranslate(SedimentReachTranslate): + def __init__(self): + super(SedimentProfileTranslate, self).__init__() -def retranslate(): - table_headers["x"] = _translate("SedimentLayers", "X (m)") - table_headers["y"] = _translate("SedimentLayers", "Y (m)") - table_headers["z"] = _translate("SedimentLayers", "Z (m)") - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["sl"] = _translate("SedimentLayers", "Sediment layers") + self._sub_dict["table_headers"] = { + "x": _translate("SedimentLayers", "X (m)"), + "y": _translate("SedimentLayers", "Y (m)"), + "z": _translate("SedimentLayers", "Z (m)"), + "name": _translate("SedimentLayers", "Name"), + "sl": _translate("SedimentLayers", "Sediment layers"), + } diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py index 8fdb8d7e..250a4735 100644 --- a/src/View/SedimentLayers/Reach/Table.py +++ b/src/View/SedimentLayers/Reach/Table.py @@ -18,23 +18,25 @@ from PyQt5.QtWidgets import ( ) from View.SedimentLayers.Reach.UndoCommand import * -from View.SedimentLayers.Reach.translate import * + +from View.Tools.PamhyrTable import PamhyrTableModel _translate = QCoreApplication.translate logger = logging.getLogger() class ComboBoxDelegate(QItemDelegate): - def __init__(self, study=None, parent=None): + def __init__(self, study=None, trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._study = study + self._trad = trad def createEditor(self, parent, option, index): self.editor = QComboBox(parent) self.editor.addItems( - [_translate("SedimentLayers", "Not defined")] + + [self._trad["nd"]] + list( map( lambda sl: str(sl), @@ -67,28 +69,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, study=None, reach=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._reach = reach - - def flags(self, index): - column = index.column() - - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if self._headers[column] == "sl": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return self._reach.number_profiles - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -98,11 +82,11 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._reach.profile(row).name + return self._data.profile(row).name if self._headers[column] == "kp": - return self._reach.profile(row).kp + return self._data.profile(row).kp if self._headers[column] == "sl": - value = self._reach.profile(row).sl + value = self._data.profile(row).sl if value == None: text = _translate("SedimentLayers", "Not defined") return text @@ -110,12 +94,6 @@ class TableModel(QAbstractTableModel): return QVariant() - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -135,7 +113,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SetSLCommand( - self._reach, row, new + self._data, row, new ) ) @@ -145,7 +123,7 @@ class TableModel(QAbstractTableModel): def apply_sl_each_profile(self, sl): self._undo.push( ApplySLCommand( - self._reach, sl + self._data, sl ) ) self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 2a655b7c..829bf035 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -28,10 +28,10 @@ from View.SedimentLayers.Reach.Table import * from View.SedimentLayers.Reach.Plot import Plot from View.SedimentLayers.Reach.SLDialog import SLDialog -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.SedimentLayers.Reach.translate import table_headers, retranslate +from View.SedimentLayers.Reach.translate import SedimentReachTranslate from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Profile.Window import ProfileSedimentLayersWindow @@ -57,39 +57,40 @@ class ReachSedimentLayersWindow(PamhyrWindow): title = name, study = study, config = config, + trad = SedimentReachTranslate(), parent = parent ) self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() def setup_table(self): - retranslate() + table_headers = self._trad.get_dict("table_headers") - table = self.find(QTableView, f"tableView") - self._table = TableModel( - study = self._study, - reach = self._reach, - undo = self._undo_stack, - ) - table.setModel(self._table) - - self._delegate_stricklers = ComboBoxDelegate( + self._delegate_sl = ComboBoxDelegate( study = self._study, + trad = self._trad, parent=self ) - table.setItemDelegateForColumn( - list(table_headers).index("sl"), - self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view = table, + data = self._reach, + opt_data = self._study, + table_headers = table_headers, + editable_headers = ["sl"], + delegates = {"sl": self._delegate_sl}, + trad = self._trad, + undo = self._undo_stack, ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): + def setup_plot(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( @@ -106,6 +107,7 @@ class ReachSedimentLayersWindow(PamhyrWindow): canvas = self.canvas, data = self._reach, toolbar = self.toolbar, + trad = self._trad, display_current = False ) self.plot.draw() diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py index ef45f076..34785f32 100644 --- a/src/View/SedimentLayers/Reach/translate.py +++ b/src/View/SedimentLayers/Reach/translate.py @@ -2,15 +2,20 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.translate import SedimentTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - "kp": _translate("SedimentLayers", "KP (m)"), - "sl": _translate("SedimentLayers", "Sediment layers"), -} +class SedimentReachTranslate(SedimentTranslate): + def __init__(self): + super(SedimentReachTranslate, self).__init__() -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["kp"] = _translate("SedimentLayers", "KP (m)") - table_headers["sl"] = _translate("SedimentLayers", "Sediment layers") + self._dict["nd"] = _translate("SedimentLayers", "Not defined") + self._dict["kp"] = _translate("SedimentLayers", "Kp (m)") + self._dict["height"] = _translate("SedimentLayers", "Height (m)") + + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + "kp": _translate("SedimentLayers", "KP (m)"), + "sl": _translate("SedimentLayers", "Sediment layers"), + } From a64a9867ef22bed1a328668b096fce02c4c95267 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 17:01:31 +0200 Subject: [PATCH 038/248] refactoring: refacto solver parameters --- src/Solver/Solvers.py | 6 +- src/View/MainWindow.py | 2 +- src/View/SolverParameters/Table.py | 69 ++++++---------- src/View/SolverParameters/Window.py | 15 ++-- src/View/SolverParameters/translate.py | 108 ++++++++++++------------- 5 files changed, 85 insertions(+), 115 deletions(-) diff --git a/src/Solver/Solvers.py b/src/Solver/Solvers.py index 221f1ff4..514e06c6 100644 --- a/src/Solver/Solvers.py +++ b/src/Solver/Solvers.py @@ -24,9 +24,9 @@ from Solver.Mage import Mage7, Mage8 _translate = QCoreApplication.translate solver_long_name = { - "generic": _translate("Solvers", "Generic"), - # "mage7": _translate("Solvers", "Mage version 7"), - "mage8": _translate("Solvers", "Mage version 8"), + "generic": "Generic", + # "mage7": "Mage v7", + "mage8": "Mage v8", } solver_type_list = { diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 9fee3a76..94acbe39 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -52,7 +52,7 @@ from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow -# from View.SolverParameters.Window import SolverParametersWindow +from View.SolverParameters.Window import SolverParametersWindow # from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow # from View.CheckList.Window import CheckListWindow # from View.Results.Window import ResultsWindow diff --git a/src/View/SolverParameters/Table.py b/src/View/SolverParameters/Table.py index 5aa488ab..47fa0ae9 100644 --- a/src/View/SolverParameters/Table.py +++ b/src/View/SolverParameters/Table.py @@ -34,37 +34,20 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SolverParameters.UndoCommand import * -from View.SolverParameters import translate as tr from Solver.Solvers import solver_long_name, solver_type_list +logger = logging.getLogger() + _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, data=None, undo=None, tab=""): - super(QAbstractTableModel, self).__init__() - self._headers = list(tr.table_headers.keys()) - self._data = data - self._undo = undo - self._tab = tab - self._params = self._data.get_params(self._tab) - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - - column = index.column() - - if self._headers[column] == "value": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._params) - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._tab = self._opt_data + self._lst = self._data.get_params(self._tab) def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -75,24 +58,18 @@ class TableModel(QAbstractTableModel): cname = self._headers[column] if cname == "name": - value = self._params.get(row)[cname] - if value in tr.names: - value = tr.names[value] + value = self._lst.get(row)[cname] + if value in self._trad.get_dict("names"): + value = self._trad.get_dict("names")[value] return value elif cname == "value": - value = self._params.get(row)[cname] - if value in tr.yes_no: - value = tr.yes_no[value] + value = self._lst.get(row)[cname] + if value in self._trad.get_dict("yes_no"): + value = self._trad.get_dict("yes_no")[value] return value return QVariant() - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return tr.table_headers[self._headers[section]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -102,12 +79,12 @@ class TableModel(QAbstractTableModel): try: if self._headers[column] == "value": - if value in tr.r_yes_no: - value = tr.r_yes_no[value].lower() + if value in self._trad.get_dict("r_yes_no"): + value = self._trad.get_dict("r_yes_no")[value].lower() self._undo.push( SetCommand( - self._params, row, + self._lst, row, "value", value ) ) @@ -123,7 +100,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # AddCommand( - # self._params, row + # self._lst, row # ) # ) @@ -135,7 +112,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # DelCommand( - # self._params, rows + # self._lst, rows # ) # ) @@ -147,7 +124,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # SortCommand( - # self._params, False + # self._lst, False # ) # ) @@ -164,7 +141,7 @@ class TableModel(QAbstractTableModel): # self._undo_stack.push( # MoveCommand( - # self._params, "up", row + # self._lst, "up", row # ) # ) @@ -172,7 +149,7 @@ class TableModel(QAbstractTableModel): # self.layoutChanged.emit() # def move_down(self, index, parent=QModelIndex()): - # if row > len(self._params): + # if row > len(self._lst): # return # target = row @@ -181,7 +158,7 @@ class TableModel(QAbstractTableModel): # self._undo_stack.push( # MoveCommand( - # self._params, "down", row + # self._lst, "down", row # ) # ) diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index a3b225a6..b655cd53 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -42,7 +42,7 @@ from PyQt5.QtWidgets import ( from View.SolverParameters.UndoCommand import * from View.SolverParameters.Table import TableModel -from View.SolverParameters import translate as tr +from View.SolverParameters.translate import ParamTranslate from Solver.Solvers import solver_long_name, solver_type_list @@ -55,16 +55,13 @@ class SolverParametersWindow(PamhyrWindow): _pamhyr_name = "Solver parameters" def __init__(self, study=None, config=None, parent=None): - # Init tanslate dictionary - tr.init() - name = self._pamhyr_name + " - " + study.name - super(SolverParametersWindow, self).__init__( title = name, study = study, config = config, options = ['copy'], + trad = ParamTranslate(), parent = parent ) @@ -73,8 +70,6 @@ class SolverParametersWindow(PamhyrWindow): self.setup_sc() self.setup_table() - self.ui.setWindowTitle(self._title) - def setup_sc(self): self._undo_stack = {} @@ -101,9 +96,13 @@ class SolverParametersWindow(PamhyrWindow): # Create table model self._table[st] = TableModel( + table_view = table, data = self._study.river, + opt_data = st, + table_headers = self._trad.get_dict("table_headers"), + editable_headers = ["value"], + trad = self._trad, undo = self._undo_stack[st], - tab = st, ) table.setModel(self._table[st]) diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 2508629e..01c51a70 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -18,66 +18,60 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = None -yes_no = None -r_yes_no = None -names = None +class ParamTranslate(PamhyrTranslate): + def __init__(self): + super(ParamTranslate, self).__init__() -def init(): - global table_headers - table_headers = { - "name": _translate("SolverParameters", "Name"), - "value": _translate("SolverParameters", "Value") - } + self._sub_dict["table_headers"] = { + "name": _translate("SolverParameters", "Name"), + "value": _translate("SolverParameters", "Value") + } - # Used to translate user parameter with value yes or no - global yes_no - yes_no = { - "yes": _translate("SolverParameters", "Yes"), - "no": _translate("SolverParameters", "No"), - "y": _translate("SolverParameters", "Y"), - "n": _translate("SolverParameters", "N"), - } + self._sub_dict["yes_no"] = { + "yes": _translate("SolverParameters", "Yes"), + "no": _translate("SolverParameters", "No"), + "y": _translate("SolverParameters", "Y"), + "n": _translate("SolverParameters", "N"), + } - # Reverse - global r_yes_no - r_yes_no = { - _translate("SolverParameters", "Yes"): "yes", - _translate("SolverParameters", "No"): "no", - _translate("SolverParameters", "Y"): "y", - _translate("SolverParameters", "N"): "n", - } + self._sub_dict["r_yes_no"] = { + _translate("SolverParameters", "Yes"): "yes", + _translate("SolverParameters", "No"): "no", + _translate("SolverParameters", "Y"): "y", + _translate("SolverParameters", "N"): "n", + } - global names - names = { - "all_init_time": _translate("SolverParameters", "Initial time (jj:hh:mm:ss)"), - "all_final_time": _translate("SolverParameters", "Final time (jj:hh:mm:ss)"), - "all_timestep": _translate("SolverParameters", "Timestep (second)"), - "all_command_line_arguments": _translate("SolverParameters", "Command line arguments"), - # Mage specific parameters - "mage_min_timestep": _translate("SolverParameters", "Minimum timestep (second)"), - "mage_timestep_tra": _translate("SolverParameters", "Time step of writing on .TRA"), - "mage_timestep_bin": _translate("SolverParameters", "Time step of writing on .BIN"), - "mage_implicitation": _translate("SolverParameters", "Implicitation parameter"), - "mage_continuity_discretization": _translate("SolverParameters", "Continuity discretization type (S/L)"), - "mage_qsj_discretization": _translate("SolverParameters", "QSJ discretization (A/B)"), - "mage_stop_criterion_iterations": _translate("SolverParameters", "Stop criterion iterations (G/A/R)"), - "mage_iteration_type": _translate("SolverParameters", "Iteration type"), - "mage_smooth_coef": _translate("SolverParameters", "Smoothing coefficient"), - "mage_cfl_max": _translate("SolverParameters", "Maximun accepted number of CFL"), - "mage_min_height": _translate("SolverParameters", "Minimum water height (meter)"), - "mage_max_niter": _translate("SolverParameters", "Maximun number of iterations (< 100)"), - "mage_timestep_reduction_factor": _translate("SolverParameters", "Timestep reduction factor"), - "mage_precision_reduction_factor_Z": _translate("SolverParameters", "Reduction precision factor of Z"), - "mage_precision_reduction_factor_Q": _translate("SolverParameters", "Reduction precision factor of Q"), - "mage_precision_reduction_factor_r": _translate("SolverParameters", "Reduction precision factor of residue"), - "mage_niter_max_precision": _translate("SolverParameters", "Number of iteration at maximum precision"), - "mage_niter_before_switch": _translate("SolverParameters", "Number of iteration before switch"), - "mage_max_froude": _translate("SolverParameters", "Maximum accepted Froude number"), - "mage_diffluence_node_height_balance": _translate("SolverParameters", "Diffluence node height balance"), - "mage_compute_reach_volume_balance": _translate("SolverParameters", "Compute reach volume balance (Y/N)"), - "mage_max_reach_volume_balance": _translate("SolverParameters", "Maximum reach volume balance"), - "mage_min_reach_volume_to_check": _translate("SolverParameters", "Minimum reach volume to check"), - } + self._sub_dict["names"] = { + "all_init_time": _translate("SolverParameters", "Initial time (jj:hh:mm:ss)"), + "all_final_time": _translate("SolverParameters", "Final time (jj:hh:mm:ss)"), + "all_timestep": _translate("SolverParameters", "Timestep (second)"), + "all_command_line_arguments": _translate("SolverParameters", "Command line arguments"), + # Mage specific parameters + "mage_min_timestep": _translate("SolverParameters", "Minimum timestep (second)"), + "mage_timestep_tra": _translate("SolverParameters", "Time step of writing on .TRA"), + "mage_timestep_bin": _translate("SolverParameters", "Time step of writing on .BIN"), + "mage_implicitation": _translate("SolverParameters", "Implicitation parameter"), + "mage_continuity_discretization": _translate("SolverParameters", "Continuity discretization type (S/L)"), + "mage_qsj_discretization": _translate("SolverParameters", "QSJ discretization (A/B)"), + "mage_stop_criterion_iterations": _translate("SolverParameters", "Stop criterion iterations (G/A/R)"), + "mage_iteration_type": _translate("SolverParameters", "Iteration type"), + "mage_smooth_coef": _translate("SolverParameters", "Smoothing coefficient"), + "mage_cfl_max": _translate("SolverParameters", "Maximun accepted number of CFL"), + "mage_min_height": _translate("SolverParameters", "Minimum water height (meter)"), + "mage_max_niter": _translate("SolverParameters", "Maximun number of iterations (< 100)"), + "mage_timestep_reduction_factor": _translate("SolverParameters", "Timestep reduction factor"), + "mage_precision_reduction_factor_Z": _translate("SolverParameters", "Reduction precision factor of Z"), + "mage_precision_reduction_factor_Q": _translate("SolverParameters", "Reduction precision factor of Q"), + "mage_precision_reduction_factor_r": _translate("SolverParameters", "Reduction precision factor of residue"), + "mage_niter_max_precision": _translate("SolverParameters", "Number of iteration at maximum precision"), + "mage_niter_before_switch": _translate("SolverParameters", "Number of iteration before switch"), + "mage_max_froude": _translate("SolverParameters", "Maximum accepted Froude number"), + "mage_diffluence_node_height_balance": _translate("SolverParameters", "Diffluence node height balance"), + "mage_compute_reach_volume_balance": _translate("SolverParameters", "Compute reach volume balance (Y/N)"), + "mage_max_reach_volume_balance": _translate("SolverParameters", "Maximum reach volume balance"), + "mage_min_reach_volume_to_check": _translate("SolverParameters", "Minimum reach volume to check"), + } From ba087f12a5d4dbc34463893a86b8e782a4bcf12d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 10:40:18 +0200 Subject: [PATCH 039/248] refactoring: Apply table refacto on checklist. --- src/View/CheckList/Table.py | 28 +++------------------------- src/View/CheckList/Window.py | 10 ++++------ src/View/MainWindow.py | 4 ++-- src/View/Tools/PamhyrTable.py | 12 +++++++++++- 4 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/View/CheckList/Table.py b/src/View/CheckList/Table.py index 5810c4be..b355eb6c 100644 --- a/src/View/CheckList/Table.py +++ b/src/View/CheckList/Table.py @@ -35,25 +35,12 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, data=None): - super(QAbstractTableModel, self).__init__() - self._headers = ["name", "status"] - self._data = data - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - return options - - def rowCount(self, parent): - return len(self._data) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): row = index.row() column = index.column() @@ -81,12 +68,3 @@ class TableModel(QAbstractTableModel): return self._data[row].summary return QVariant() - - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return self._headers[section] - - return QVariant() - - def update(self): - self.layoutChanged.emit() diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index b02522bc..baf99983 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -40,6 +40,7 @@ from PyQt5.QtWidgets import ( from View.CheckList.Table import TableModel from View.CheckList.Worker import Worker +from View.CheckList.Translate import CheckListTranslate _translate = QCoreApplication.translate @@ -61,6 +62,7 @@ class CheckListWindow(PamhyrWindow): title = name, study = study, config = config, + trad = CheckListTranslate(), options = [], parent = parent ) @@ -78,16 +80,12 @@ class CheckListWindow(PamhyrWindow): def setup_table(self): table = self.find(QTableView, f"tableView") - self._table = TableModel( + table_view = table, + table_headers = self._trad.get_dict("table_headers"), data = self._checker_list, ) - table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) - def setup_progress_bar(self): self._progress = self.find(QProgressBar, f"progressBar") self._p = 0 # Progress current step diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 94acbe39..20f4f065 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -53,8 +53,8 @@ from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow from View.SolverParameters.Window import SolverParametersWindow -# from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -# from View.CheckList.Window import CheckListWindow +from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow +from View.CheckList.Window import CheckListWindow # from View.Results.Window import ResultsWindow from View.Debug.Window import ReplWindow diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 65cd7687..d809dced 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QPushButton, QLineEdit, QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, - QComboBox, QStyledItemDelegate, + QComboBox, QStyledItemDelegate, QHeaderView, ) logger = logging.getLogger() @@ -100,10 +100,17 @@ class PamhyrTableModel(QAbstractTableModel): self._setup_delegates() self._setup_lst() + self._table_view_configure() def _setup_lst(self): self._lst = self._data + def _table_view_configure(self): + self._table_view.setModel(self) + self._table_view.setSelectionBehavior(QAbstractItemView.SelectRows) + self._table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self._table_view.setAlternatingRowColors(True) + def flags(self, index): column = index.column() @@ -140,3 +147,6 @@ class PamhyrTableModel(QAbstractTableModel): def redo(self): self._undo.redo() self.layoutChanged.emit() + + def update(self): + self.layoutChanged.emit() From 2ff4f63d23ffc1c7ce4b4206d68111c878af6237 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 14:25:44 +0200 Subject: [PATCH 040/248] refactoring: refactor Results. --- doc/dev/documentation.org | 2 +- src/Model/Results/Results.py | 4 ++ src/Model/Results/River/River.py | 9 +++++ src/View/CheckList/Translate.py | 32 +++++++++++++++ src/View/MainWindow.py | 2 +- src/View/Results/PlotAC.py | 18 +++++---- src/View/Results/PlotH.py | 18 +++++---- src/View/Results/PlotKPC.py | 18 +++++---- src/View/Results/PlotSedProfile.py | 18 +++++---- src/View/Results/PlotSedReach.py | 18 +++++---- src/View/Results/PlotXY.py | 18 +++++---- src/View/Results/Table.py | 65 +++++++++--------------------- src/View/Results/Window.py | 40 +++++------------- src/View/Results/translate.py | 24 +++++------ 14 files changed, 149 insertions(+), 137 deletions(-) create mode 100644 src/View/CheckList/Translate.py diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 8ca3ff05..e50fe58e 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -671,7 +671,7 @@ method to draw the plot from scratch, the second to update the plot if data has changed. #+begin_src python :python python3 :results output :noweb yes - from View.Plot.APlot import APlot + from View.Tools.PamhyrPlot import PamhyrPlot class MyPlot(APlot): def __init__(self, canvas=None, data=None, toolbar=None): diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 67698f4e..3258b0e1 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -43,6 +43,10 @@ class Results(object): def river(self): return self._river + @property + def study(self): + return self._study + def set(self, key, value): self._meta_data[key] = value diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 9f72f731..80cd8450 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -26,6 +26,9 @@ class Profile(object): self._profile = profile # Source profile in the study self._data = {} # Dict of dict {: {: , ...}, ...} + def __len__(self): + return len(self._data) + @property def name(self): return self._profile.name @@ -66,6 +69,9 @@ class Reach(object): ) ) + def __len__(self): + return len(self._profiles) + @property def name(self): return self._reach.name @@ -91,6 +97,9 @@ class River(object): # Dict with timestamps as key self._reachs = [] + def __len__(self): + return len(self._reachs) + @property def reachs(self): return self._reachs.copy() diff --git a/src/View/CheckList/Translate.py b/src/View/CheckList/Translate.py new file mode 100644 index 00000000..b3ce856d --- /dev/null +++ b/src/View/CheckList/Translate.py @@ -0,0 +1,32 @@ +# 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 . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + +class CheckListTranslate(PamhyrTranslate): + def __init__(self): + super(CheckListTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("CheckList", "Name"), + "status": _translate("CheckList", "Status"), + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 20f4f065..e2a8859c 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -55,7 +55,7 @@ from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow -# from View.Results.Window import ResultsWindow +from View.Results.Window import ResultsWindow from View.Debug.Window import ReplWindow from Model.Study import Study diff --git a/src/View/Results/PlotAC.py b/src/View/Results/PlotAC.py index f0e4b0e2..ed3d99ad 100644 --- a/src/View/Results/PlotAC.py +++ b/src/View/Results/PlotAC.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,16 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotAC(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotAC, self).__init__( - canvas=canvas, - data=results, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._current_timestamp = max(results.get("timestamps")) diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py index 557c7cec..99a59f88 100644 --- a/src/View/Results/PlotH.py +++ b/src/View/Results/PlotH.py @@ -22,7 +22,7 @@ from functools import reduce from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -32,14 +32,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotH(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotH(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotH, self).__init__( - canvas=canvas, - data=results, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._mode = "time" diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py index 68f6fb4e..61de649d 100644 --- a/src/View/Results/PlotKPC.py +++ b/src/View/Results/PlotKPC.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,16 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotKPC(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotKPC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotKPC, self).__init__( - canvas=canvas, - data=results, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._current_timestamp = max(results.get("timestamps")) diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py index 62f4e290..3163931b 100644 --- a/src/View/Results/PlotSedProfile.py +++ b/src/View/Results/PlotSedProfile.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,14 +15,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotSedProfile(APlot): - def __init__(self, canvas=None, study=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotSedProfile(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotSedProfile, self).__init__( - canvas=canvas, - data=study, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._results = results diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py index 657b70e8..c63c80ec 100644 --- a/src/View/Results/PlotSedReach.py +++ b/src/View/Results/PlotSedReach.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,14 +15,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotSedReach(APlot): - def __init__(self, canvas=None, study=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotSedReach(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotSedReach, self).__init__( - canvas=canvas, - data=study, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._results = results diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index 3e7a88f3..42716776 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -21,7 +21,7 @@ import logging from functools import reduce from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -31,14 +31,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotXY(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None, display_current=True): +class PlotXY(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + display_current=True, parent=None): super(PlotXY, self).__init__( - canvas=canvas, - data=results, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self.display_current = display_current diff --git a/src/View/Results/Table.py b/src/View/Results/Table.py index 8e167459..f4d7b81a 100644 --- a/src/View/Results/Table.py +++ b/src/View/Results/Table.py @@ -34,6 +34,7 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel from View.Results.translate import * logger = logging.getLogger() @@ -41,33 +42,16 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, results = None, study = None, mode = None, undo=None): - super(QAbstractTableModel, self).__init__() +class TableModel(PamhyrTableModel): + def _setup_lst(self): + _river = self._data.river + if self._opt_data == "reach": + self._lst = _river.reachs + else: + self._lst = _river.reach(0).profiles - self._results = results - self._study = study - self._mode = mode - self._undo_stack = undo - - self._table_headers = table_headers_reach - if mode != "reach": - self._table_headers = table_headers_profile - - self._headers = list(self._table_headers.keys()) - - self._selected = 0 - self._timestamp = 0 - - def rowCount(self, parent=QModelIndex()): - if self._mode == "reach": - return len(self._results.river.reachs) - - current_reach = self._results.river.reach(self._selected) - return len(current_reach.profiles) - - def columnCount(self, parent=QModelIndex()): - return len(self._headers) + def __init__(self, **kwargs): + super(TableModel, self).__init__(**kwargs) def data(self, index, role=Qt.DisplayRole): if role != Qt.ItemDataRole.DisplayRole: @@ -76,32 +60,23 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._mode == "reach": + if self._opt_data == "reach": if self._headers[column] == "name": - v = self._results.river.reach(row).name + v = self._lst[row].name return str(v) else: - current_reach = self._results.river.reach(self._selected) if self._headers[column] == "name": - v = current_reach.profile(row).name + v = self._lst[row].name return str(v) elif self._headers[column] == "kp": - v = current_reach.profile(row).kp + v = self._lst[row].kp return f"{v:.4f}" return QVariant() - def headerData(self, section, orientation, role=Qt.DisplayRole): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return self._table_headers[self._headers[section]] - - return QVariant() - - def index(self, row, column, parent=QModelIndex()): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - return self.createIndex(row, column, QModelIndex()) - - def flags(self, index): - return Qt.ItemIsEnabled | Qt.ItemIsSelectable + def update(self, reach): + _river = self._data.river + if self._opt_data == "reach": + self._lst = _river.reachs + else: + self._lst = _river.reach(reach).profiles diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 4009c836..61e9f22b 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -41,8 +41,8 @@ from PyQt5.QtWidgets import ( QSlider, QLabel, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.Results.PlotXY import PlotXY from View.Results.PlotAC import PlotAC @@ -52,10 +52,7 @@ from View.Results.PlotSedReach import PlotSedReach from View.Results.PlotSedProfile import PlotSedProfile from View.Results.Table import TableModel -from View.Results.translate import ( - table_headers_reach, table_headers_profile, - retranslate, -) +from View.Results.translate import ResultsTranslate from View.Stricklers.Window import StricklersWindow _translate = QCoreApplication.translate @@ -85,42 +82,27 @@ class ResultsWindow(PamhyrWindow): title = name, study = study, config = config, + trad = ResultsTranslate(), parent=parent ) self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_slider() self.setup_statusbar() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._solver.name + " - " - + self._results.date - ) - def setup_table(self): - retranslate() self._table = {} - for t in ["reach", "profile"]: table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( - results = self._results, - study = self._study, - mode = t, + table_view = table, + table_headers = self._trad.get_dict(f"table_headers_{t}"), + data = self._results, undo = self._undo_stack, + opt_data = t ) - table.setModel(self._table[t]) - - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) def setup_slider(self): self._slider_profile = self.find(QSlider, f"verticalSlider_profile") @@ -132,7 +114,7 @@ class ResultsWindow(PamhyrWindow): self._slider_time.setMaximum(len(self._timestamps) - 1) self._slider_time.setValue(len(self._timestamps) - 1) - def setup_graph(self): + def setup_plot(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( @@ -219,7 +201,6 @@ class ResultsWindow(PamhyrWindow): self.plot_sed_reach = PlotSedReach( canvas = self.canvas_5, results = self._results, - study = self._study, reach_id = 0, profile_id = 0, toolbar = self.toolbar_5 @@ -239,7 +220,6 @@ class ResultsWindow(PamhyrWindow): self.plot_sed_profile = PlotSedProfile( canvas = self.canvas_6, results = self._results, - study = self._study, reach_id = 0, profile_id = 0, toolbar = self.toolbar_6 diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py index 37351b66..14f55ba3 100644 --- a/src/View/Results/translate.py +++ b/src/View/Results/translate.py @@ -18,19 +18,19 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers_reach = { - "name": _translate("Results", "Reach name"), -} +class ResultsTranslate(PamhyrTranslate): + def __init__(self): + super(ResultsTranslate, self).__init__() -table_headers_profile = { - "name": _translate("Results", "Name"), - "kp": _translate("Results", "KP (m)"), -} + self._sub_dict["table_headers_reach"] = { + "name": _translate("Results", "Reach name"), + } -def retranslate(): - table_headers_reach["name"] = _translate("Results", "Reach name") - - table_headers_profile["name"] = _translate("Results", "Name") - table_headers_profile["kp"] = _translate("Results", "KP (m)") + self._sub_dict["table_headers_profile"] = { + "name": _translate("Results", "Name"), + "kp": _translate("Results", "KP (m)"), + } From b1c658d7116614c173144fbe6feb54c4ec7e6c56 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 14:34:15 +0200 Subject: [PATCH 041/248] refactoring: Delete deprecated unused files. --- .../Geometry/Profile/mainwindow_ui_profile.py | 260 ------------- src/View/Geometry/mainwindow_ui_reach.py | 357 ------------------ 2 files changed, 617 deletions(-) delete mode 100644 src/View/Geometry/Profile/mainwindow_ui_profile.py delete mode 100644 src/View/Geometry/mainwindow_ui_reach.py diff --git a/src/View/Geometry/Profile/mainwindow_ui_profile.py b/src/View/Geometry/Profile/mainwindow_ui_profile.py deleted file mode 100644 index a6844e55..00000000 --- a/src/View/Geometry/Profile/mainwindow_ui_profile.py +++ /dev/null @@ -1,260 +0,0 @@ -# mainwindow_ui_profile.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 . - -# -*- coding: utf-8 -*- - -import os.path - -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import Qt, QSize -from PyQt5.QtWidgets import ( - QTableView, QAbstractItemView, QHeaderView, - QToolBar, -) - -from View.Plot import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar - -file_path = os.path.abspath(os.path.dirname(__file__)) -icon_path = f"{os.path.dirname(os.path.dirname(file_path))}/../View/ui/ressources/" - -class Ui_MainWindow(object): - def setupUi(self, MainWindowProfile): - MainWindowProfile.setObjectName("MainWindowProfile") - MainWindowProfile.resize(1089, 699) - self.centralwidget = QtWidgets.QWidget(MainWindowProfile) - self.centralwidget.setObjectName("centralwidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.centralwidget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.splitter = QtWidgets.QSplitter(self.centralwidget) - self.splitter.setOrientation(Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.widget = QtWidgets.QWidget(self.splitter) - self.widget.setObjectName("widget") - self.verticalLayout_left = QtWidgets.QVBoxLayout(self.widget) - self.verticalLayout_left.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_left.setObjectName("verticalLayout_left") - - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - - self.toolBar = QToolBar(MainWindowProfile) - self.toolBar.setOrientation(Qt.Vertical) - self.toolBar.setEnabled(True) - MainWindowProfile.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) - - btn_size = QSize(40, 28) - - self.btn_add = QtWidgets.QAction(self.widget) - self.btn_add.setObjectName("btn_add") - icon_btn_add = QtGui.QIcon() - icon_btn_add.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-add.png")) - self.btn_add.setIcon(icon_btn_add) - self.toolBar.addAction(self.btn_add) - - self.btn_delete = QtWidgets.QAction(self.widget) - self.btn_delete.setObjectName("btn_delete") - icon_btn_delete = QtGui.QIcon() - icon_btn_delete.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-remove.png")) - self.btn_delete.setIcon(icon_btn_delete) - self.toolBar.addAction(self.btn_delete) - - self.btn_sort_asc_x = QtWidgets.QAction(self.widget) - self.btn_sort_asc_x.setObjectName("btn_sort_asc_x") - icon_btn_sort_asc_x = QtGui.QIcon() - icon_btn_sort_asc_x.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc_x.setIcon(icon_btn_sort_asc_x) - self.toolBar.addAction(self.btn_sort_asc_x) - - self.btn_sort_desc_x = QtWidgets.QAction(self.widget) - self.btn_sort_desc_x.setObjectName("btn_sort_desc_x") - icon_btn_sort_desc_x = QtGui.QIcon() - icon_btn_sort_desc_x.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc_x.setIcon(icon_btn_sort_desc_x) - self.toolBar.addAction(self.btn_sort_desc_x) - - self.btn_sort_asc_y = QtWidgets.QAction(self.widget) - self.btn_sort_asc_y.setObjectName("btn_sort_asc_y") - icon_btn_sort_asc_y = QtGui.QIcon() - icon_btn_sort_asc_y.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc_y.setIcon(icon_btn_sort_asc_y) - self.toolBar.addAction(self.btn_sort_asc_y) - - self.btn_sort_desc_y = QtWidgets.QAction(self.widget) - self.btn_sort_desc_y.setObjectName("btn_sort_desc_y") - icon_btn_sort_desc_y = QtGui.QIcon() - icon_btn_sort_desc_y.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc_y.setIcon(icon_btn_sort_desc_y) - self.toolBar.addAction(self.btn_sort_desc_y) - - self.btn_move_up = QtWidgets.QAction(self.widget) - self.btn_move_up.setObjectName("btn_move_up") - icon_btn_move_up = QtGui.QIcon() - icon_btn_move_up.addPixmap( - QtGui.QPixmap(f"{icon_path}up.png")) - self.btn_move_up.setIcon(icon_btn_move_up) - self.toolBar.addAction(self.btn_move_up) - - self.btn_move_down = QtWidgets.QAction(self.widget) - self.btn_move_down.setObjectName("btn_move_down") - icon_btn_move_down = QtGui.QIcon() - icon_btn_move_down.addPixmap( - QtGui.QPixmap(f"{icon_path}down.png")) - self.btn_move_down.setIcon(icon_btn_move_down) - self.toolBar.addAction(self.btn_move_down) - - self.btn_export = QtWidgets.QAction(self.widget) - self.btn_export.setObjectName("btn_export") - icon_btn_export = QtGui.QIcon() - icon_btn_export.addPixmap( - QtGui.QPixmap(f"{icon_path}export.png")) - self.btn_export.setIcon(icon_btn_export) - self.toolBar.addAction(self.btn_export) - - self.btn_copy = QtWidgets.QAction(self.widget) - self.btn_copy.setObjectName("btn_copy") - icon_btn_copy = QtGui.QIcon() - icon_btn_copy.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-copy.png")) - self.btn_copy.setIcon(icon_btn_copy) - self.toolBar.addAction(self.btn_copy) - - self.btn_paste = QtWidgets.QAction(self.widget) - self.btn_paste.setObjectName("btn_paste") - icon_btn_paste = QtGui.QIcon() - icon_btn_paste.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-paste.png")) - self.btn_paste.setIcon(icon_btn_paste) - self.toolBar.addAction(self.btn_paste) - - self.btn_check = QtWidgets.QAction(self.widget) - self.btn_check.setObjectName("btn_check") - icon_btn_check = QtGui.QIcon() - icon_btn_check.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-apply.png")) - self.btn_check.setIcon(icon_btn_check) - self.toolBar.addAction(self.btn_check) - - self.btn_go_back = QtWidgets.QAction(self.widget) - self.btn_go_back.setObjectName("btn_go_back") - icon_btn_go_back = QtGui.QIcon() - icon_btn_go_back.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-undo.png")) - self.btn_go_back.setIcon(icon_btn_go_back) - self.toolBar.addAction(self.btn_go_back) - - self.btn_reset = QtWidgets.QAction(self.widget) - self.btn_reset.setObjectName("btn_reset") - icon_btn_reset = QtGui.QIcon() - icon_btn_reset.addPixmap( - QtGui.QPixmap(f"{icon_path}cancel.png")) - self.btn_reset.setIcon(icon_btn_reset) - self.toolBar.addAction(self.btn_reset) - - self.verticalLayout_left.addLayout(self.horizontalLayout) - - self.tableView = QTableView(self.widget) - self.tableView.setObjectName("tableView") - self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) - self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) - self.tableView.resizeColumnsToContents() - self.tableView.resizeRowsToContents() - self.tableView.setAlternatingRowColors(True) - self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.verticalLayout_left.addWidget(self.tableView) - - self.widget1 = QtWidgets.QWidget(self.splitter) - self.widget1.setObjectName("widget1") - - self.verticalLayout_right = QtWidgets.QVBoxLayout(self.widget1) - self.verticalLayout_right.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_right.setObjectName("verticalLayout_right") - - self.label_graphic_help = QtWidgets.QLabel(self.widget1) - self.label_graphic_help.setMaximumSize(QtCore.QSize(16777215, 25)) - self.label_graphic_help.setBaseSize(QtCore.QSize(0, 30)) - self.label_graphic_help.setObjectName("label_graphic_help") - self.verticalLayout_right.addWidget(self.label_graphic_help) - - self.canvas = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas.setObjectName("canvas") - - self.my_toolbar = PamhyrPlotToolbar( - self.canvas, self.widget1 - ) - - self.verticalLayout_right.addWidget(self.my_toolbar) - self.verticalLayout_right.addWidget(self.canvas) - self.horizontalLayout_2.addWidget(self.splitter) - - MainWindowProfile.setCentralWidget(self.centralwidget) - - self.menubar = QtWidgets.QMenuBar(MainWindowProfile) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1089, 20)) - self.menubar.setObjectName("menubar") - MainWindowProfile.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindowProfile) - self.statusbar.setObjectName("statusbar") - MainWindowProfile.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindowProfile) - QtCore.QMetaObject.connectSlotsByName(MainWindowProfile) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindowProfile", "MainWindowProfile")) - self.btn_add.setToolTip(_translate("MainWindowProfile", "Insérer un point")) - self.btn_delete.setToolTip( - _translate("MainWindowProfile", "Supprimer le/les point(s) sélectionnés")) - self.btn_sort_asc_x.setToolTip( - _translate("MainWindowProfile", "Trier les points par ordre croissant de X")) - self.btn_sort_desc_x.setToolTip( - _translate("MainWindowProfile", "Trier les points par ordre décroissant de X")) - self.btn_sort_asc_y.setToolTip(_translate("MainWindowProfile", - "Trier les points par ordre croissant de Y" - )) - self.btn_sort_desc_y.setToolTip(_translate("MainWindowProfile", - "Trier les points par ordre décroissant de Y")) - self.btn_move_up.setToolTip(_translate("MainWindowProfile", - "Décaler le point sélectionné vers le haut")) - self.btn_move_down.setToolTip(_translate("MainWindowProfile", - "Décaler le point sélectionné vers le bas")) - self.btn_export.setToolTip(_translate("MainWindowProfile", - "Exporter (dans un fichier) les points du profil au format tabulé")) - self.btn_copy.setToolTip( - _translate("MainWindowProfile", "Copier la sélection au format tabulé")) - self.btn_paste.setToolTip(_translate("MainWindowProfile", - "Coller la sélection depuis le presse-papier au format tabulé")) - self.btn_check.setToolTip(_translate("MainWindowProfile", - "Vérifier la validité de la saisie et garder ou pas les modifications " - "apportées")) - self.btn_go_back.setToolTip(_translate("MainWindowProfile", - "Annuler toutes les modifications depuis la dernière validation")) - self.btn_reset.setToolTip(_translate("MainWindowProfile", - "Annuler toutes les modifications et revenir à l\'état initial")) - self.label_graphic_help.setToolTip(_translate("MainWindowProfile", - "Ligne d'eau : \n \tZ : Cote (m) \n \tA : Aire mouillée (" - "m\u00B2) \n \tp : " - "Périmètre mouillé (m) \n \tL : Largeur au miroir (m)")) - self.label_graphic_help.setText( - _translate("MainWindowProfile", "\'Maj + Clic\' : Ligne d\'eau & \'Ctrl + Clic\' : " - "Sélectionner des points")) diff --git a/src/View/Geometry/mainwindow_ui_reach.py b/src/View/Geometry/mainwindow_ui_reach.py deleted file mode 100644 index ff0797ab..00000000 --- a/src/View/Geometry/mainwindow_ui_reach.py +++ /dev/null @@ -1,357 +0,0 @@ -# mainwindow_ui_reach.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 . - -# -*- coding: utf-8 -*- - -import sys -import os.path - -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import QSize, Qt -from PyQt5.QtWidgets import ( - QAbstractItemView, QHeaderView, QToolBar, -) - -from View.Plot import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar - - -file_path = os.path.abspath(os.path.dirname(__file__)) -icon_path = f"{os.path.dirname(os.path.dirname(file_path))}/View/ui/ressources/" - -class Ui_MainWindow(object): - def setupUi(self, MainWindow_reach): - MainWindow_reach.setObjectName("MainWindow_reach") - MainWindow_reach.resize(1400, 800) - self.centralwidget = QtWidgets.QWidget(MainWindow_reach) - self.centralwidget.setObjectName("centralwidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.centralwidget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.splitter_3 = QtWidgets.QSplitter(self.centralwidget) - self.splitter_3.setOrientation(Qt.Horizontal) - self.splitter_3.setObjectName("splitter_3") - self.widget = QtWidgets.QWidget(self.splitter_3) - self.widget.setObjectName("widget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.widget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - - # self.label_section_order = QtWidgets.QLabel(self.widget) - # self.label_section_order.setLayoutDirection(Qt.LeftToRight) - # self.label_section_order.setObjectName("label_section_order") - - # self.verticalLayout.addWidget(self.label_section_order) - - self.toolBar = QToolBar(MainWindow_reach) - self.toolBar.setEnabled(True) - MainWindow_reach.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) - - btn_size = QSize(40, 28) - - self.btn_open = QtWidgets.QAction(self.widget) - self.btn_open.setObjectName("btn_open") - icon_btn_open = QtGui.QIcon() - icon_btn_open.addPixmap( - QtGui.QPixmap(f"{icon_path}open.png")) - self.btn_open.setIcon(icon_btn_open) - self.toolBar.addAction(self.btn_open) - - self.btn_add = QtWidgets.QAction(self.widget) - self.btn_add.setObjectName("btn_add") - icon_btn_add = QtGui.QIcon() - icon_btn_add.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk_add.png")) - self.btn_add.setIcon(icon_btn_add) - self.toolBar.addAction(self.btn_add) - - self.btn_delete = QtWidgets.QAction(self.widget) - self.btn_delete.setObjectName("btn_delete") - icon_btn_delete = QtGui.QIcon() - icon_btn_delete.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-remove.png")) - self.btn_delete.setIcon(icon_btn_delete) - self.toolBar.addAction(self.btn_delete) - - self.btn_edit = QtWidgets.QAction(self.widget) - self.btn_edit.setObjectName("btn_edit") - icon_btn_edit = QtGui.QIcon() - icon_btn_edit.addPixmap( - QtGui.QPixmap(f"{icon_path}gnome-stock-edit.png")) - self.btn_edit.setIcon(icon_btn_edit) - self.toolBar.addAction(self.btn_edit) - - # self.btn_copy_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_copy_selected_profile.setObjectName("btn_copy_selected_profile") - # icon_btn_copy_selected_profile = QtGui.QIcon() - # icon_btn_copy_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-copy.png")) - # self.btn_copy_selected_profile.setIcon(icon_btn_copy_selected_profile) - # self.toolBar.addAction(self.btn_copy_selected_profile) - - # self.btn_paste_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_paste_selected_profile.setObjectName("btn_paste_selected_profile") - # icon_btn_paste_selected_profile = QtGui.QIcon() - # icon_btn_paste_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-paste.png")) - # self.btn_paste_selected_profile.setIcon(icon_btn_paste_selected_profile) - # self.toolBar.addAction(self.btn_paste_selected_profile) - - # self.btn_duplicate_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_duplicate_selected_profile.setObjectName("btn_duplicate_selected_profile") - # icon_btn_duplicate_selected_profile = QtGui.QIcon() - # icon_btn_duplicate_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-dnd-multiple.png")) - # self.btn_duplicate_selected_profile.setIcon(icon_btn_duplicate_selected_profile) - # self.toolBar.addAction(self.btn_duplicate_selected_profile) - - self.btn_sort_asc = QtWidgets.QAction(self.widget) - self.btn_sort_asc.setObjectName("btn_sort_asc") - icon_btn_sort_asc = QtGui.QIcon() - icon_btn_sort_asc.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc.setIcon(icon_btn_sort_asc) - self.toolBar.addAction(self.btn_sort_asc) - - self.btn_sort_desc = QtWidgets.QAction(self.widget) - self.btn_sort_desc.setObjectName("btn_sort_desc") - icon_btn_sort_desc = QtGui.QIcon() - icon_btn_sort_desc.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc.setIcon(icon_btn_sort_desc) - self.toolBar.addAction(self.btn_sort_desc) - - self.btn_move_up = QtWidgets.QAction(self.widget) - self.btn_move_up.setObjectName("btn_move_up") - icon_btn_move_up = QtGui.QIcon() - icon_btn_move_up.addPixmap( - QtGui.QPixmap(f"{icon_path}up.png")) - self.btn_move_up.setIcon(icon_btn_move_up) - self.toolBar.addAction(self.btn_move_up) - - self.btn_move_down = QtWidgets.QAction(self.widget) - self.btn_move_down.setObjectName("btn_move_down") - icon_btn_move_down = QtGui.QIcon() - icon_btn_move_down.addPixmap( - QtGui.QPixmap(f"{icon_path}down.png")) - self.btn_move_down.setIcon(icon_btn_move_down) - self.toolBar.addAction(self.btn_move_down) - - # self.btn_end_editing = QtWidgets.QAction(self.widget) - # self.btn_end_editing.setObjectName("btn_end_editing") - # icon_btn_save = QtGui.QIcon() - # icon_btn_save.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-apply.png")) - # self.btn_end_editing.setIcon(icon_btn_save) - # self.toolBar.addAction(self.btn_end_editing) - - self.verticalLayout.addLayout(self.horizontalLayout) - self.tableView = QtWidgets.QTableView(self.widget) - self.tableView.setObjectName("tableView") - self.verticalLayout.addWidget(self.tableView) - self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) - self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.tableView.setAlternatingRowColors(True) - self.tableView.verticalHeader().hide() - self.tableView.resizeColumnsToContents() - self.tableView.resizeRowsToContents() - - self.widget1 = QtWidgets.QWidget(self.splitter_3) - self.widget1.setObjectName("widget1") - self.gridLayout = QtWidgets.QGridLayout(self.widget1) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.splitter_2 = QtWidgets.QSplitter(self.widget1) - self.splitter_2.setOrientation(Qt.Vertical) - self.splitter_2.setObjectName("splitter_2") - self.splitter = QtWidgets.QSplitter(self.splitter_2) - self.splitter.setOrientation(Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.verticalLayoutWidget_3 = QtWidgets.QWidget(self.splitter) - self.verticalLayoutWidget_3.setObjectName("verticalLayoutWidget_3") - self.verticalLayout_canvas_1 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget_3) - self.verticalLayout_canvas_1.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_1.setObjectName("verticalLayout_canvas_1") - - self.canvas_1 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_1.setObjectName("canvas_1") - - self.toolbar_1 = PamhyrPlotToolbar( - self.canvas_1, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_1.addWidget(self.toolbar_1) - self.verticalLayout_canvas_1.addWidget(self.canvas_1) - - self.verticalLayoutWidget_2 = QtWidgets.QWidget(self.splitter) - self.verticalLayoutWidget_2.setObjectName("verticalLayoutWidget_2") - self.verticalLayout_canvas_2 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget_2) - self.verticalLayout_canvas_2.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_2.setObjectName("verticalLayout_canvas_2") - - self.canvas_2 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_2.setObjectName("canvas_2") - - self.toolbar_2 = PamhyrPlotToolbar( - self.canvas_2, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_2.addWidget(self.toolbar_2) - self.verticalLayout_canvas_2.addWidget(self.canvas_2) - self.verticalLayoutWidget = QtWidgets.QWidget(self.splitter_2) - self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") - self.verticalLayout_canvas_3 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) - self.verticalLayout_canvas_3.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_3.setObjectName("verticalLayout_canvas_3") - - self.canvas_3 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_3.setObjectName("canvas_3") - - self.toolbar_3 = PamhyrPlotToolbar( - self.canvas_3, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_3.addWidget(self.toolbar_3) - self.verticalLayout_canvas_3.addWidget(self.canvas_3) - - self.gridLayout.addWidget(self.splitter_2, 0, 0, 3, 3) - self.verticalSlider = QtWidgets.QSlider(self.widget1) - self.verticalSlider.setMinimumSize(QtCore.QSize(30, 0)) - self.verticalSlider.setOrientation(Qt.Vertical) - self.verticalSlider.setInvertedAppearance(True) - self.verticalSlider.setInvertedControls(False) - self.verticalSlider.setObjectName("verticalSlider") - self.gridLayout.addWidget(self.verticalSlider, 0, 3, 1, 1) - - self.btn_slider_up = QtWidgets.QPushButton(self.widget1) - self.btn_slider_up.setMaximumSize(btn_size) - self.btn_slider_up.setText("") - self.btn_slider_up.setObjectName("btn_slider_up") - icon_btn_slider_up = QtGui.QIcon() - icon_btn_slider_up.addPixmap( - QtGui.QPixmap(f"{icon_path}go-up2.png")) - self.btn_slider_up.setIcon(icon_btn_slider_up) - #self.btn_slider_up.setIconSize(btn_size) - self.gridLayout.addWidget(self.btn_slider_up, 1, 3, 1, 1) - - self.btn_slider_down = QtWidgets.QPushButton(self.widget1) - self.btn_slider_down.setMaximumSize(btn_size) - self.btn_slider_down.setText("") - self.btn_slider_down.setObjectName("btn_slider_down") - icon_btn_slider_down = QtGui.QIcon() - icon_btn_slider_down.addPixmap( - QtGui.QPixmap(f"{icon_path}go-down1.png")) - self.btn_slider_down.setIcon(icon_btn_slider_down) - #self.btn_slider_down.setIconSize(btn_size) - self.gridLayout.addWidget(self.btn_slider_down, 2, 3, 1, 1) - - self.vertical_slider_label = QtWidgets.QLabel(self.widget1) - self.vertical_slider_label.setObjectName("vertical_slider_label") - self.gridLayout.addWidget(self.vertical_slider_label, 3, 2, 2, 1) - self.vertical_slider_label.setAlignment(Qt.AlignRight) - - # self.btn_slider_go_back = QtWidgets.QPushButton(self.widget1) - # self.btn_slider_go_back.setMaximumSize(btn_size) - # self.btn_slider_go_back.setText("") - # self.btn_slider_go_back.setObjectName("btn_slider_go_back") - # icon_btn_slider_go_back = QtGui.QIcon() - # icon_btn_slider_go_back.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-go-back.png")) - # self.btn_slider_go_back.setIcon(icon_btn_slider_go_back) - # #self.btn_slider_go_back.setIconSize(btn_size) - # self.gridLayout.addWidget(self.btn_slider_go_back, 5, 0, 1, 1) - # self.btn_slider_go_back.setEnabled(False) - - # self.btn_slider_go_forward = QtWidgets.QPushButton(self.widget1) - # self.btn_slider_go_forward.setMaximumSize(btn_size) - # self.btn_slider_go_forward.setText("") - # self.btn_slider_go_forward.setObjectName("btn_slider_go_forward") - # icon_btn_slider_go_forward = QtGui.QIcon() - # icon_btn_slider_go_forward.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-go-forward.png")) - # self.btn_slider_go_forward.setIcon(icon_btn_slider_go_forward) - # #self.btn_slider_go_forward.setIconSize(btn_size) - # self.gridLayout.addWidget(self.btn_slider_go_forward, 5, 1, 1, 1) - # self.btn_slider_go_forward.setEnabled(False) - - # self.horizontalSlider = QtWidgets.QSlider(self.widget1) - # self.horizontalSlider.setMinimumSize(QtCore.QSize(0, 30)) - # self.horizontalSlider.setOrientation(Qt.Horizontal) - # self.horizontalSlider.setObjectName("horizontalSlider") - # self.gridLayout.addWidget(self.horizontalSlider, 5, 2, 1, 1) - # self.horizontalSlider.setEnabled(False) - - self.horizontalLayout_2.addWidget(self.splitter_3) - MainWindow_reach.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow_reach) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1123, 20)) - self.menubar.setObjectName("menubar") - MainWindow_reach.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow_reach) - self.statusbar.setObjectName("statusbar") - MainWindow_reach.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindow_reach) - QtCore.QMetaObject.connectSlotsByName(MainWindow_reach) - - def retranslateUi(self, MainWindow_reach): - _translate = QtCore.QCoreApplication.translate - self.name_reach = "" - self.mainwindow_title = _translate("MainWindow_reach", "Geometry reach edit") - MainWindow_reach.setWindowTitle(self.mainwindow_title + f"{self.name_reach}") - # self.label_section_order.setText(f"{self.text_section_order}") - self.vertical_slider_value = "" - self.vertical_slider_label.setText(_translate("MainWindow_reach", "Kp = ") + f"{self.vertical_slider_value}") - self.btn_open.setToolTip(_translate("MainWindow_reach", " Import geometry")) - self.btn_add.setToolTip(_translate("MainWindow_reach", " New profile")) - self.btn_delete.setToolTip(_translate("MainWindow_reach", " Delete selected profile")) - self.btn_edit.setToolTip(_translate("MainWindow_reach", " Editer selected profile")) - - self.btn_sort_asc.setToolTip(_translate("MainWindow_reach", " Trier les profils par ordre croissant des Pk")) - self.btn_sort_desc.setToolTip(_translate("MainWindow_reach", " Trier les profils par ordre décroissant des Pk")) - self.btn_move_up.setToolTip(_translate("MainWindow_reach", " Changer l'ordre des profils (en décalant le " - "profil sélectionné vers le haut)")) - self.btn_move_down.setToolTip(_translate("MainWindow_reach", " Changer l'ordre des profils (en décalant le " - "profil sélectionné vers le bas)")) - # self.btn_end_editing.setToolTip(_translate("MainWindow_reach", " Terminer l'édition")) - self.tableView_header = [_translate("MainWindow_reach", "Name"), - _translate("MainWindow_reach", "Kp (m)"), - _translate("MainWindow_reach", "Type")] - - self.toolbar_1._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+Z")) - self.toolbar_1._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+Z)")) - - self.toolbar_2._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+E")) - self.toolbar_2._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+E)")) - - self.toolbar_3._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+R")) - self.toolbar_3._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+R)")) - - self.toolbar_1._actions["non_isometric_view"].setShortcut("Alt+S") - self.toolbar_1._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+S)")) - - self.toolbar_2._actions["non_isometric_view"].setShortcut("Alt+D") - self.toolbar_2._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+D)")) - - self.toolbar_3._actions["non_isometric_view"].setShortcut("Alt+F") - self.toolbar_3._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+F)")) From 1a47bbe19d7fa7d8c2ac41d095a52566863cdd49 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 14:54:17 +0200 Subject: [PATCH 042/248] Results: Allow to open last results from mainwindow. --- src/View/MainWindow.py | 14 +++++++++++++- src/View/RunSolver/Window.py | 7 ++++++- src/View/ui/MainWindow.ui | 13 ++++++------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index e2a8859c..23c26b51 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -111,6 +111,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self._study = None # Results + self._last_solver = None self._last_results = None # UI @@ -150,6 +151,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ + logger.debug(f"Set {action} to {enable}") self.findChild(QAction, action).setEnabled(enable) def setup_sc(self): @@ -180,6 +182,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_run_solver": self.run_solver, "action_menu_sediment_layers": self.open_sediment_layers, "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, + "action_menu_results_last": self.open_last_results, ## Help "action_menu_about": self.open_about, # ToolBar action @@ -325,9 +328,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): for action in model_action: self.enable_actions(action, not no_model) - def set_results(self, results): + def set_results(self, solver, results): + self._last_solver = solver self._last_results = results + self.enable_actions("action_menu_results_last", True) + ############ # FEATURES # ############ @@ -703,6 +709,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): else: res.activateWindow() + def open_last_results(self): + if self._last_solver is None or self._last_results is None: + return + + self.open_solver_results(self._last_solver, self._last_results) + ######### # DEBUG # ######### diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 72a65d45..62484308 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -204,6 +204,11 @@ class SolverLogWindow(PamhyrWindow): if self._solver.log_file() != "": self.find(QAction, "action_log_file").setEnabled(True) + # Get results + if self._results is None: + self._results = self._solver.results(self._study, self._workdir, qlog = self._output) + self._parent.set_results(self._solver, self._results) + while self._output.qsize() != 0: s = self._output.get() if type(s) is str and "[ERROR]" in s: @@ -256,7 +261,7 @@ class SolverLogWindow(PamhyrWindow): if self._results is None: self._results = self._solver.results(self._study, self._workdir, qlog = self._output) - self._parent.set_results(self._results) + self._parent.set_results(self._solver, self._results) self._parent.open_solver_results(self._solver, self._results) def log_file(self): diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index ea77d698..7578d600 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -141,12 +141,11 @@ - + - &Plots + &Results - - + @@ -178,7 +177,7 @@ - + @@ -597,12 +596,12 @@ Close - + false - Hydrograph + Visualize last results From 559d7dc00680106da6f99df96eba7bf7f52b8879 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 15:54:27 +0200 Subject: [PATCH 043/248] doc: Keep doc html and add doc window in Pamhyr2. --- .gitlab-ci.yml | 3 ++ doc/users/images/.gitkeep | 0 src/View/Doc/Window.py | 70 +++++++++++++++++++++++++++++++++++++++ src/View/MainWindow.py | 17 ++++++++++ src/View/ui/MainWindow.ui | 31 +++++++++++++++-- src/View/ui/WebView.ui | 50 ++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 doc/users/images/.gitkeep create mode 100644 src/View/Doc/Window.py create mode 100644 src/View/ui/WebView.ui diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7f2aecdf..7b58f4f4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -98,6 +98,8 @@ build-users-doc: artifacts: paths: - doc/users/documentation.pdf + - doc/users/documentation.html + - doc/users/images/ build-developers-doc: stage: build @@ -121,6 +123,7 @@ build-developers-doc: - doc/dev/documentation.pdf - doc/dev/documentation.html - doc/dev/html.tar + - doc/dev/images/ build-linux: stage: build diff --git a/doc/users/images/.gitkeep b/doc/users/images/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py new file mode 100644 index 00000000..73416322 --- /dev/null +++ b/src/View/Doc/Window.py @@ -0,0 +1,70 @@ +# 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 . + +# -*- coding: utf-8 -*- + +import os +import logging + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate +logger = logging.getLogger() + + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtCore import QUrl +from PyQt5.QtWebKitWidgets import QWebView +from PyQt5.QtWebKit import QWebSettings + +class DocWindow(PamhyrWindow): + _pamhyr_ui = "WebView" + _pamhyr_name = "Doc" + + def _path_file(self, filename): + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "doc", filename + ) + ) + + def __init__(self, filename=None, + study=None, config=None, + parent=None): + super(DocWindow, self).__init__( + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent + + ) + + self.setup_setting() + self.setup_url(filename) + + def setup_url(self, filename): + webView = self.find(QWebView, "webView") + webView.setUrl(QUrl(f"file://{self._path_file(filename)}")) + + def setup_setting(self): + webView = self.find(QWebView, "webView") + settings = webView.settings() + settings.setAttribute(QWebSettings.PluginsEnabled, True) + settings.setAttribute(QWebSettings.JavascriptEnabled, False) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 23c26b51..f304c144 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -56,6 +56,7 @@ from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow from View.Results.Window import ResultsWindow +from View.Doc.Window import DocWindow from View.Debug.Window import ReplWindow from Model.Study import Study @@ -183,6 +184,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_sediment_layers": self.open_sediment_layers, "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, "action_menu_results_last": self.open_last_results, + "action_menu_Pamhyr": self.open_doc_user, + "action_menu_Pamhyr_dev": self.open_doc_dev, ## Help "action_menu_about": self.open_about, # ToolBar action @@ -715,6 +718,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.open_solver_results(self._last_solver, self._last_results) + def open_doc(self, filename): + doc = DocWindow( + filename = filename, + parent = self + ) + doc.show() + + + def open_doc_user(self): + self.open_doc("Pamhyr2-users.html") + + def open_doc_dev(self): + self.open_doc("Pamhyr2-dev.html") + ######### # DEBUG # ######### diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 7578d600..9fd3b229 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -160,8 +160,15 @@ &Help - - + + + Help + + + + + + @@ -895,6 +902,26 @@ Edit reach sediment layers + + + Doc + + + + + Pamhyr2 users + + + + + Pamhyr2 developer + + + + + Mage + + diff --git a/src/View/ui/WebView.ui b/src/View/ui/WebView.ui new file mode 100644 index 00000000..d29a0c89 --- /dev/null +++ b/src/View/ui/WebView.ui @@ -0,0 +1,50 @@ + + + MainWindow + + + + 0 + 0 + 640 + 480 + + + + MainWindow + + + + + + + + about:blank + + + + + + + + + + 0 + 0 + 640 + 22 + + + + + + + + QWebView + QWidget +
QtWebKitWidgets/QWebView
+
+
+ + +
From a77c342e699f4b8ddd8e48236274f006441167a7 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 16:21:23 +0200 Subject: [PATCH 044/248] doc: Fix window display for version PyQt5 > 5.5 --- requirements.txt | 3 ++- src/View/Doc/Window.py | 26 ++++++++++++++------------ src/View/ui/WebView.ui | 15 +-------------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/requirements.txt b/requirements.txt index 729dbbe4..01d9a29a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ PyQt5==5.15.4 PyQt5-Qt5==5.15.2 -PyQt5-sip==12.8.1 +PyQt5-sip==12.12.2 +PyQtWebEngine==5.15.6 pyqtgraph>=0.12.1 matplotlib>=3.4.1 numpy>=1.24.2 diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py index 73416322..fec3b3aa 100644 --- a/src/View/Doc/Window.py +++ b/src/View/Doc/Window.py @@ -27,10 +27,9 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout from PyQt5.QtCore import QUrl -from PyQt5.QtWebKitWidgets import QWebView -from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings class DocWindow(PamhyrWindow): _pamhyr_ui = "WebView" @@ -56,15 +55,18 @@ class DocWindow(PamhyrWindow): ) - self.setup_setting() + self.setup_web_engine() self.setup_url(filename) - def setup_url(self, filename): - webView = self.find(QWebView, "webView") - webView.setUrl(QUrl(f"file://{self._path_file(filename)}")) + def setup_web_engine(self): + vl = self.find(QVBoxLayout, "verticalLayout") + self._web_view = QWebEngineView() - def setup_setting(self): - webView = self.find(QWebView, "webView") - settings = webView.settings() - settings.setAttribute(QWebSettings.PluginsEnabled, True) - settings.setAttribute(QWebSettings.JavascriptEnabled, False) + settings = self._web_view.settings() + settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) + settings.setAttribute(QWebEngineSettings.JavascriptEnabled, False) + + vl.addWidget(self._web_view) + + def setup_url(self, filename): + self._web_view.setUrl(QUrl(f"file://{self._path_file(filename)}")) diff --git a/src/View/ui/WebView.ui b/src/View/ui/WebView.ui index d29a0c89..0576f1a3 100644 --- a/src/View/ui/WebView.ui +++ b/src/View/ui/WebView.ui @@ -16,13 +16,7 @@ - - - - about:blank - - - + @@ -38,13 +32,6 @@ - - - QWebView - QWidget -
QtWebKitWidgets/QWebView
-
-
From f4d580a000d492fae4ec8d05d62ee71139239950 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 11:11:23 +0200 Subject: [PATCH 045/248] doc: Doc in format html or pdf, back/forward button and firefox button. --- packages/linux.sh | 41 ++++++++++++++++++++++---------- src/View/Doc/Window.py | 31 +++++++++++++++++++++--- src/View/MainWindow.py | 14 ++++++----- src/View/Tools/ASubWindow.py | 21 +++++++++++++--- src/View/ui/MainWindow.ui | 37 +++++++++++++++++++++++------ src/View/ui/WebView.ui | 46 +++++++++++++++++++++++++++++++++--- 6 files changed, 155 insertions(+), 35 deletions(-) diff --git a/packages/linux.sh b/packages/linux.sh index 215047e8..9a5787a7 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -43,18 +43,21 @@ cd $OLD_PWD echo " *** COPY DATA" -mkdir -p dist/pamhyr/View/ui -mkdir -p dist/pamhyr/View/ui/Widgets -cp -r ../src/View/ui/ressources/ dist/pamhyr/View/ui/ -cp -r ../src/View/ui/Widgets/*.ui dist/pamhyr/View/ui/Widgets/ -cp -r ../src/View/ui/*.ui dist/pamhyr/View/ui/ +mkdir -p dist/pamhyr/_internal/View/ui +mkdir -p dist/pamhyr/_internal/View/ui/Widgets +cp -r ../src/View/ui/ressources/ dist/pamhyr/_internal/View/ui/ +cp -r ../src/View/ui/Widgets/*.ui dist/pamhyr/_internal/View/ui/Widgets/ +cp -r ../src/View/ui/*.ui dist/pamhyr/_internal/View/ui/ mkdir -p dist/pamhyr/lang cp -r ../src/lang/*.qm dist/pamhyr/lang/ -cp ../VERSION dist/pamhyr/ -cp ../AUTHORS dist/pamhyr/ -cp ../LICENSE dist/pamhyr/ +mkdir dist/pamhyr/_internal/ +#cp ../VERSION dist/pamhyr/_internal/ +VERSION=$(cat ../VERSION) +echo "local-build-$VERSION" > dist/pamhyr/_internal/VERSION +cp ../AUTHORS dist/pamhyr/_internal/ +cp ../LICENSE dist/pamhyr/_internal/ mkdir -p dist/pamhyr/mage/ cp ../mage/mage dist/pamhyr/mage/ @@ -65,6 +68,18 @@ mkdir -p dist/pamhyr/tests_cases/ mkdir -p dist/pamhyr/tests_cases/Saar cp ../tests_cases/Saar/Saar.pamhyr dist/pamhyr/tests_cases/Saar/ +mkdir -p dist/pamhyr/doc/ +cp ../doc/dev/documentation.pdf dist/pamhyr/doc/Pamhyr2-dev.pdf +cp ../doc/dev/documentation.html dist/pamhyr/doc/Pamhyr2-dev.html + +cp ../doc/users/documentation.pdf dist/pamhyr/doc/Pamhyr2-users.pdf +cp ../doc/users/documentation.html dist/pamhyr/doc/Pamhyr2-users.html + +mkdir -p dist/pamhyr/doc/images +cp ../doc/users/images/* dist/pamhyr/doc/images/ +cp ../doc/dev/images/* dist/pamhyr/doc/images/ +cp ../doc/images/* dist/pamhyr/doc/images/ + echo " *** MAKE SRC PACKAGE" OLD_PWD=$PWD @@ -76,11 +91,11 @@ mv ../pamhyr-src.tar.gz ./ echo " *** MAKE BIN PACKAGE" -OLD_PWD=$PWD -cd dist/ -tar --xz -cf pamhyr-gnulinux-amd64.tar.xz pamhyr --checkpoint=.100 -cd $OLD_PWD +# OLD_PWD=$PWD +# cd dist/ +# tar --xz -cf pamhyr-gnulinux-amd64.tar.xz pamhyr --checkpoint=.100 +# cd $OLD_PWD -mv dist/pamhyr-gnulinux-amd64.tar.xz ./ +# mv dist/pamhyr-gnulinux-amd64.tar.xz ./ echo " *** DONE" diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py index fec3b3aa..3c39dca0 100644 --- a/src/View/Doc/Window.py +++ b/src/View/Doc/Window.py @@ -18,6 +18,7 @@ import os import logging +import subprocess from View.Tools.PamhyrWindow import PamhyrWindow @@ -27,7 +28,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QAction from PyQt5.QtCore import QUrl from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings @@ -55,8 +56,18 @@ class DocWindow(PamhyrWindow): ) + self._path = self._path_file(filename) + self.setup_web_engine() - self.setup_url(filename) + self.setup_url(self._path) + self.setup_connection() + + def setup_connection(self): + self.findChild(QAction, "action_back").triggered.connect(self.back) + self.findChild(QAction, "action_forward").triggered.connect(self.forward) + + self.findChild(QAction, "action_firefox").triggered.connect(self.open_in_firefox) + def setup_web_engine(self): vl = self.find(QVBoxLayout, "verticalLayout") @@ -64,9 +75,23 @@ class DocWindow(PamhyrWindow): settings = self._web_view.settings() settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) - settings.setAttribute(QWebEngineSettings.JavascriptEnabled, False) + settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) + settings.setAttribute(QWebEngineSettings.PdfViewerEnabled, True) vl.addWidget(self._web_view) def setup_url(self, filename): + logger.info(f"Open documentation : {filename}") self._web_view.setUrl(QUrl(f"file://{self._path_file(filename)}")) + + def back(self): + self._web_view.back() + + def forward(self): + self._web_view.forward() + + def open_in_firefox(self): + _ = subprocess.Popen( + f"firefox {self._path}", + shell = True + ) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index f304c144..f8067ae6 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -184,9 +184,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_sediment_layers": self.open_sediment_layers, "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, "action_menu_results_last": self.open_last_results, - "action_menu_Pamhyr": self.open_doc_user, - "action_menu_Pamhyr_dev": self.open_doc_dev, ## Help + "action_menu_pamhyr_users_pdf": lambda: self.open_doc_user(ext="pdf"), + "action_menu_pamhyr_developers_pdf": lambda: self.open_doc_dev(ext="pdf"), + "action_menu_pamhyr_users_html": lambda: self.open_doc_user(ext="html"), + "action_menu_pamhyr_developers_html": lambda: self.open_doc_dev(ext="html"), "action_menu_about": self.open_about, # ToolBar action "action_toolBar_quit": self.close, @@ -726,11 +728,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): doc.show() - def open_doc_user(self): - self.open_doc("Pamhyr2-users.html") + def open_doc_user(self, ext="pdf"): + self.open_doc(f"Pamhyr2-users.{ext}") - def open_doc_dev(self): - self.open_doc("Pamhyr2-dev.html") + def open_doc_dev(self, ext="pdf"): + self.open_doc(f"Pamhyr2-dev.{ext}") ######### # DEBUG # diff --git a/src/View/Tools/ASubWindow.py b/src/View/Tools/ASubWindow.py index 9ac63c2d..b6adcc82 100644 --- a/src/View/Tools/ASubWindow.py +++ b/src/View/Tools/ASubWindow.py @@ -467,7 +467,12 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): super(ASubMainWindow, self).__init__(parent=parent) if ui is not None: self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "..", "ui", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", f"{ui}.ui" + ) + ), self ) @@ -499,7 +504,12 @@ class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubWindow, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "..", "ui", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", f"{ui}.ui" + ) + ), self ) self.name = name @@ -530,7 +540,12 @@ class AWidget(QWidget, ASubWindowFeatures): def __init__(self, ui="", parent=None): super(AWidget, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "..", "ui", "Widgets", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", "Widgets", f"{ui}.ui" + ) + ), self ) self.parent = parent diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 9fd3b229..951dadc9 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -164,8 +164,16 @@ Help - - + + + Pamhyr2 + + + + + + + @@ -907,11 +915,6 @@ Doc - - - Pamhyr2 users - - Pamhyr2 developer @@ -922,6 +925,26 @@ Mage + + + Users (pdf) + + + + + Users (html) + + + + + Developers (pdf) + + + + + Developers (html) + + diff --git a/src/View/ui/WebView.ui b/src/View/ui/WebView.ui index 0576f1a3..7d0a41bf 100644 --- a/src/View/ui/WebView.ui +++ b/src/View/ui/WebView.ui @@ -6,8 +6,8 @@ 0 0 - 640 - 480 + 800 + 450 @@ -25,12 +25,52 @@ 0 0 - 640 + 800 22 + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + Firefox + + + Open document in Firefox + + + + + + ressources/gtk-go-back.pngressources/gtk-go-back.png + + + back + + + + + + ressources/gtk-go-forward.pngressources/gtk-go-forward.png + + + forward + + From 421185573217431cbd26350160a314ef4e728784 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 12:05:29 +0200 Subject: [PATCH 046/248] ci: Update ci build instruction. --- .gitlab-ci.yml | 29 +++++++++++++++++------------ packages/make-windows-dir.bat | 28 +++++++++++++++++----------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7b58f4f4..381c127f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -155,34 +155,39 @@ build-linux: - mkdir -p pamhyr - mkdir -p pamhyr/pamhyr # Copy Pamhyr info - - cp -v ../VERSION pamhyr/pamhyr/ - - cp -v ../AUTHORS pamhyr/pamhyr/ - - cp -v ../LICENSE pamhyr/pamhyr/ + - cp -v ../VERSION pamhyr/pamhyr/_internal/ + - cp -v ../AUTHORS pamhyr/pamhyr/_internal/ + - cp -v ../LICENSE pamhyr/pamhyr/_internal/ # Copy mage - mkdir -p pamhyr/mage - cp -v ../mage-linux/* pamhyr/mage/ # Copy Pamhyr - cp -r dist/pamhyr pamhyr/ # Copy Pamhyr resources - - mkdir -p pamhyr/pamhyr/View - - mkdir -p pamhyr/pamhyr/View/ui - - mkdir -p pamhyr/pamhyr/View/ui/Widgets - - cp -r ../src/View/ui/*.ui pamhyr/pamhyr/View/ui/ - - cp -r ../src/View/ui/ressources/ pamhyr/pamhyr/View/ui - - cp -r ../src/View/ui/Widgets/*.ui pamhyr/pamhyr/View/ui/Widgets + - mkdir -p pamhyr/pamhyr/_internal/View + - mkdir -p pamhyr/pamhyr/_internal/View/ui + - mkdir -p pamhyr/pamhyr/_internal/View/ui/Widgets + - cp -r ../src/View/ui/*.ui pamhyr/pamhyr/_internal/View/ui/ + - cp -r ../src/View/ui/ressources/ pamhyr/pamhyr/_internal/View/ui + - cp -r ../src/View/ui/Widgets/*.ui pamhyr/pamhyr/_internal/View/ui/Widgets # Copy lang - - mkdir -p pamhyr/pamhyr/lang - - cp -r ../src/lang/*.qm pamhyr/pamhyr/lang + - mkdir -p pamhyr/pamhyr/_internal/lang + - cp -r ../src/lang/*.qm pamhyr/pamhyr/_internal/lang # Copy tests cases - mkdir -p pamhyr/tests_cases/ - mkdir -p pamhyr/tests_cases/Saar - cp ../tests_cases/Saar/Saar.pamhyr pamhyr/tests_cases/Saar/ # Copy documentations - mkdir -p pamhyr/doc/ + - mkdir -p pamhyr/doc/images/ - mkdir -p pamhyr/doc/html/ - cp ../doc/users/documentation.pdf pamhyr/doc/Pamhyr2-users.pdf + - cp ../doc/users/documentation.html pamhyr/doc/Pamhyr2-users.html - cp ../doc/dev/documentation.pdf pamhyr/doc/Pamhyr2-dev.pdf - - cp ../doc/dev/documentation.html pamhyr/doc/html/dev.html + - cp ../doc/dev/documentation.html pamhyr/doc/Pamhyr2-dev.html + - cp ../doc/users/images/* pamhyr/doc/images + - cp ../doc/dev/images/* pamhyr/doc/images + - cp ../doc/images/* pamhyr/doc/images # Create running script - echo "#!/bin/sh" > pamhyr/Pamhyr2 - echo "./pamhyr/pamhyr $@" >> pamhyr/Pamhyr2 diff --git a/packages/make-windows-dir.bat b/packages/make-windows-dir.bat index 1045b4d4..0f35ba28 100644 --- a/packages/make-windows-dir.bat +++ b/packages/make-windows-dir.bat @@ -16,26 +16,26 @@ rem along with this program. If not, see . @ECHO ON -mkdir pamhyr\pamhyr +mkdir pamhyr\pamhyr\_internal xcopy /s /e /y dist\pamhyr pamhyr\pamhyr rem Copy data -mkdir pamhyr\pamhyr\lang -mkdir pamhyr\pamhyr\View\ui\ressources -mkdir pamhyr\pamhyr\View\ui\Widgets +mkdir pamhyr\pamhyr\_internal\lang +mkdir pamhyr\pamhyr\_internal\View\ui\ressources +mkdir pamhyr\pamhyr\_internal\View\ui\Widgets rem UI -copy /y ..\src\View\ui\ressources\ pamhyr\pamhyr\View\ui\ressources -copy /y ..\src\View\ui\Widgets\*.ui pamhyr\pamhyr\View\ui\Widgets -copy /y ..\src\View\ui\*.ui pamhyr\pamhyr\View\ui\ +copy /y ..\src\View\ui\ressources\ pamhyr\pamhyr\_internal\View\ui\ressources +copy /y ..\src\View\ui\Widgets\*.ui pamhyr\pamhyr\_internal\View\ui\Widgets +copy /y ..\src\View\ui\*.ui pamhyr\pamhyr\_internal\View\ui\ rem Lang -copy /y ..\src\lang\*.qm pamhyr\pamhyr\lang\ +copy /y ..\src\lang\*.qm pamhyr\pamhyr\_internal\lang\ rem Information -copy /y ..\VERSION pamhyr\pamhyr -copy /y ..\AUTHORS pamhyr\pamhyr -copy /y ..\LICENSE pamhyr\pamhyr +copy /y ..\VERSION pamhyr\pamhyr\_internal +copy /y ..\AUTHORS pamhyr\pamhyr\_internal +copy /y ..\LICENSE pamhyr\pamhyr\_internal rem MAGE mkdir pamhyr\mage @@ -50,5 +50,11 @@ copy /y ..\tests_cases\Saar\Saar.pamhyr pamhyr\tests_cases\Saar\ rem Documentations mkdir pamhyr\doc +mkdir pamhyr\doc\images copy /y ..\doc\users\documentation.pdf pamhyr\doc\Pamhyr2-users.pdf copy /y ..\doc\dev\documentation.pdf pamhyr\doc\Pamhyr2-dev.pdf +copy /y ..\doc\users\documentation.html pamhyr\doc\Pamhyr2-users.html +copy /y ..\doc\dev\documentation.html pamhyr\doc\Pamhyr2-dev.html +copy /y ..\doc\users\images\* pamhyr\doc\images +copy /y ..\doc\dev\images\* pamhyr\doc\images +copy /y ..\doc\images\* pamhyr\doc\images From 820adf2bb7f70b555388f45f9720b084e43da56b Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 12:08:58 +0200 Subject: [PATCH 047/248] ci: Fix linux ci. --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 381c127f..af79eeef 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -154,6 +154,7 @@ build-linux: # Create directory - mkdir -p pamhyr - mkdir -p pamhyr/pamhyr + - mkdir -p pamhyr/pamhyr/_internal # Copy Pamhyr info - cp -v ../VERSION pamhyr/pamhyr/_internal/ - cp -v ../AUTHORS pamhyr/pamhyr/_internal/ From 24cb32c96cd0f08a0474d18590ead152e840c26a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 13:44:15 +0200 Subject: [PATCH 048/248] ci: Fix linux ci. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index af79eeef..8bb1e367 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -186,7 +186,7 @@ build-linux: - cp ../doc/users/documentation.html pamhyr/doc/Pamhyr2-users.html - cp ../doc/dev/documentation.pdf pamhyr/doc/Pamhyr2-dev.pdf - cp ../doc/dev/documentation.html pamhyr/doc/Pamhyr2-dev.html - - cp ../doc/users/images/* pamhyr/doc/images + #- cp ../doc/users/images/* pamhyr/doc/images - cp ../doc/dev/images/* pamhyr/doc/images - cp ../doc/images/* pamhyr/doc/images # Create running script From 2825d02cb257b81f845329469e96e9fe32cf613c Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 14:16:15 +0200 Subject: [PATCH 049/248] doc: Fix path for build version. --- src/View/Doc/Window.py | 12 ++++++++++-- src/init.py | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py index 3c39dca0..ce6077ea 100644 --- a/src/View/Doc/Window.py +++ b/src/View/Doc/Window.py @@ -16,7 +16,7 @@ # -*- coding: utf-8 -*- -import os +import os, sys import logging import subprocess @@ -37,10 +37,18 @@ class DocWindow(PamhyrWindow): _pamhyr_name = "Doc" def _path_file(self, filename): + if ".py" in sys.argv[0]: + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "doc", filename + ) + ) + return os.path.abspath( os.path.join( os.path.dirname(__file__), - "..", "..", "..", "doc", filename + "..", "..", "..", "..", "doc", filename ) ) diff --git a/src/init.py b/src/init.py index 0d602128..5e5e434d 100644 --- a/src/init.py +++ b/src/init.py @@ -83,6 +83,10 @@ def license(): logger.info("license: This is free software, and you are welcome to redistribute it") logger.info("license: under certain conditions.") + py_version = sys.version.split("\n")[0] + + logger.info(f"python: {py_version}") + def setup_lang(app, conf:Config): "Return QTranslator configured for current language" lang_file = "" From 7365bf70b61ac3671f58379b24b0e53131448809 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 14:51:19 +0200 Subject: [PATCH 050/248] doc: Add mage documentation open action. --- src/View/Doc/Window.py | 3 ++- src/View/MainWindow.py | 27 +++++++++++++++++++-------- src/View/ui/MainWindow.ui | 4 ++-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py index ce6077ea..4be981b8 100644 --- a/src/View/Doc/Window.py +++ b/src/View/Doc/Window.py @@ -36,7 +36,8 @@ class DocWindow(PamhyrWindow): _pamhyr_ui = "WebView" _pamhyr_name = "Doc" - def _path_file(self, filename): + @classmethod + def _path_file(cls, filename): if ".py" in sys.argv[0]: return os.path.abspath( os.path.join( diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 3c74158d..8539af1d 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -23,11 +23,11 @@ from queue import Queue from PyQt5 import QtGui from PyQt5.QtGui import ( - QKeySequence, + QKeySequence, QDesktopServices, ) from PyQt5.QtCore import ( - QTranslator, QEvent + QTranslator, QEvent, QUrl, ) from PyQt5.QtWidgets import ( QMainWindow, QApplication, QAction, @@ -190,6 +190,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_pamhyr_developers_pdf": lambda: self.open_doc_dev(ext="pdf"), "action_menu_pamhyr_users_html": lambda: self.open_doc_user(ext="html"), "action_menu_pamhyr_developers_html": lambda: self.open_doc_dev(ext="html"), + "action_menu_mage": self.open_doc_mage, "action_menu_about": self.open_about, # ToolBar action "action_toolBar_quit": self.close, @@ -721,13 +722,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.open_solver_results(self._last_solver, self._last_results) - def open_doc(self, filename): - doc = DocWindow( - filename = filename, - parent = self - ) - doc.show() + ################# + # DOCUMENTATION # + ################# + def open_doc(self, filename): + if ".odt" in filename: + url = QUrl(f"file://{DocWindow._path_file(filename)}") + QDesktopServices.openUrl(url) + else: + doc = DocWindow( + filename = filename, + parent = self + ) + doc.show() def open_doc_user(self, ext="pdf"): self.open_doc(f"Pamhyr2-users.{ext}") @@ -735,6 +743,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_doc_dev(self, ext="pdf"): self.open_doc(f"Pamhyr2-dev.{ext}") + def open_doc_mage(self): + self.open_doc("Mage-8.odt") + ######### # DEBUG # ######### diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 951dadc9..9cd766c4 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -174,7 +174,7 @@ - + @@ -920,7 +920,7 @@ Pamhyr2 developer - + Mage From 8da1cb622f17dda8967d5276a12e5b44cda9ff5d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 15:22:28 +0200 Subject: [PATCH 051/248] ci: Install and update pip packages. --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8bb1e367..58831384 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -148,7 +148,7 @@ build-linux: # Setup virtual env - python3 -m venv linux-venv - . linux-venv/bin/activate - - pip3 install -r ../requirements.txt + - pip3 install -U -r ../requirements.txt # Run Pyinstaller - pyinstaller -y --paths linux-venv/lib/python3.8/site-packages ../src/pamhyr.py # Create directory @@ -218,7 +218,7 @@ build-windows: - mkdir windows - cd windows # Setup env - - python -m pip install -r ..\requirements.txt + - python -m pip install -U -r ..\requirements.txt # Run Pyinstaller - pyinstaller -y ..\src\pamhyr.py # Create directory From 397b5b0cab21f32eac60f8280bd2184c2da77f00 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 16:22:16 +0200 Subject: [PATCH 052/248] debug: Display qt5 path in terminal. --- src/init.py | 11 ++++++++--- src/pamhyr.py | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/init.py b/src/init.py index 5e5e434d..99f98d93 100644 --- a/src/init.py +++ b/src/init.py @@ -20,7 +20,7 @@ import sys, os import locale import logging -from PyQt5.QtCore import QTranslator +from PyQt5.QtCore import QTranslator, QLibraryInfo from tools import ( reset_timers, display_timers, timer, @@ -57,7 +57,7 @@ except: logger.error("Failed to create logfile...") -def license(): +def legal_info(): "Display in stdout Pamhyr infomation about licence and version" blue = lambda s: logger.info(f"{logger_color_blue()}{s}{logger_color_reset()}") @@ -83,10 +83,15 @@ def license(): logger.info("license: This is free software, and you are welcome to redistribute it") logger.info("license: under certain conditions.") - py_version = sys.version.split("\n")[0] +def debug_info(): + py_version = sys.version.split("\n")[0] logger.info(f"python: {py_version}") + info = QLibraryInfo + logger.info(f"{info.location(QLibraryInfo.PrefixPath)}") + logger.info(f"{info.location(QLibraryInfo.LibraryExecutablesPath)}") + def setup_lang(app, conf:Config): "Return QTranslator configured for current language" lang_file = "" diff --git a/src/pamhyr.py b/src/pamhyr.py index 0d595df4..cdaa5360 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -37,7 +37,7 @@ from Model.Study import Study from Scripts.P3DST import Script3DST from Scripts.Hello import ScriptHello -from init import license, setup_lang +from init import legal_info, debug_info, setup_lang logger = logging.getLogger() @@ -71,7 +71,8 @@ def main(): tr = setup_lang(app, conf) app.installTranslator(tr) - license() + legal_info() + debug_info() if len(sys.argv) > 1: # Run a script From 242b1a4e50371128b5968c58a32540a76f16bef1 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 16:55:54 +0200 Subject: [PATCH 053/248] pamhyr: Put debug info as logger debug handler. --- src/init.py | 6 +++--- src/pamhyr.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/init.py b/src/init.py index 99f98d93..a6eb3aa4 100644 --- a/src/init.py +++ b/src/init.py @@ -86,11 +86,11 @@ def legal_info(): def debug_info(): py_version = sys.version.split("\n")[0] - logger.info(f"python: {py_version}") + logger.debug(f"python: {py_version}") info = QLibraryInfo - logger.info(f"{info.location(QLibraryInfo.PrefixPath)}") - logger.info(f"{info.location(QLibraryInfo.LibraryExecutablesPath)}") + logger.debug(f"Qt PrefixPath: {info.location(QLibraryInfo.PrefixPath)}") + logger.debug(f"Qt LibExecPath: {info.location(QLibraryInfo.LibraryExecutablesPath)}") def setup_lang(app, conf:Config): "Return QTranslator configured for current language" diff --git a/src/pamhyr.py b/src/pamhyr.py index cdaa5360..4226efd6 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -62,6 +62,8 @@ def gui(app:QApplication, conf:Config): application = ApplicationWindow(conf=conf) application.show() + debug_info() + return app.exec_() def main(): @@ -72,7 +74,6 @@ def main(): app.installTranslator(tr) legal_info() - debug_info() if len(sys.argv) > 1: # Run a script From 7644927de952a7b8e7f9f21fed6bf01ff0f55018 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 17:03:23 +0200 Subject: [PATCH 054/248] pamhyr: Allow Pamhyr2 script run on tty mode. --- src/pamhyr.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pamhyr.py b/src/pamhyr.py index 4226efd6..93386f06 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -68,10 +68,6 @@ def gui(app:QApplication, conf:Config): def main(): conf = Config.load() - app = QApplication(sys.argv) - - tr = setup_lang(app, conf) - app.installTranslator(tr) legal_info() @@ -81,17 +77,27 @@ def main(): if script == "help": ret = usage(sys.argv) - elif script == "gui": - ret = gui(app, conf) - elif script == "debug": - conf.debug = True + elif script == "gui" or script == "debug": + if script == "debug": + conf.debug = True + + app = QApplication(sys.argv) + tr = setup_lang(app, conf) + app.installTranslator(tr) ret = gui(app, conf) else: if script not in scripts: - logger.error(f"{logger_color_red()}Invalid script name '{sys.argv[1]}'{logger_color_reset()}") + logger.error( + f"{logger_color_red()}Invalid script name " + + f"'{sys.argv[1]}'{logger_color_reset()}" + ) usage(sys.argv) sys.exit(-1) + # By default script as no QApplication (allow run script + # in tty mode) + app = None + application = scripts[script](app, conf, sys.argv) ret = application.run() if ret != 0: From b35be1626860bce619094b6a08c2ca87372e2082 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 10 Oct 2023 09:30:40 +0200 Subject: [PATCH 055/248] ci: Fix windows exe generator script. --- packages/pamhyr.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pamhyr.nsi b/packages/pamhyr.nsi index 80ad4770..c4591745 100644 --- a/packages/pamhyr.nsi +++ b/packages/pamhyr.nsi @@ -5,7 +5,7 @@ Name "Pamhyr2" OutFile "pamhyr-windows.exe" -LicenseData "pamhyr\pamhyr\LICENSE" +LicenseData "pamhyr\pamhyr\_internal\LICENSE" LicenseText "I Agree" RequestExecutionLevel admin From d0ea629c1010dc54751f506fda2cef0c04223dbf Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 10 Oct 2023 09:36:08 +0200 Subject: [PATCH 056/248] pamhyr: Fix execution without argument. --- src/pamhyr.py | 58 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/pamhyr.py b/src/pamhyr.py index 93386f06..4ae6a7dc 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -72,39 +72,37 @@ def main(): legal_info() if len(sys.argv) > 1: - # Run a script script = sys.argv[1] - - if script == "help": - ret = usage(sys.argv) - elif script == "gui" or script == "debug": - if script == "debug": - conf.debug = True - - app = QApplication(sys.argv) - tr = setup_lang(app, conf) - app.installTranslator(tr) - ret = gui(app, conf) - else: - if script not in scripts: - logger.error( - f"{logger_color_red()}Invalid script name " + - f"'{sys.argv[1]}'{logger_color_reset()}" - ) - usage(sys.argv) - sys.exit(-1) - - # By default script as no QApplication (allow run script - # in tty mode) - app = None - - application = scripts[script](app, conf, sys.argv) - ret = application.run() - if ret != 0: - application.usage() else: - # No args, run Pamhyr2 interface + script = "gui" + + if script == "help": + ret = usage(sys.argv) + elif script == "gui" or script == "debug": + if script == "debug": + conf.debug = True + + app = QApplication(sys.argv) + tr = setup_lang(app, conf) + app.installTranslator(tr) ret = gui(app, conf) + else: + if script not in scripts: + logger.error( + f"{logger_color_red()}Invalid script name " + + f"'{sys.argv[1]}'{logger_color_reset()}" + ) + usage(sys.argv) + sys.exit(-1) + + # By default script as no QApplication (allow run script + # in tty mode) + app = None + + application = scripts[script](app, conf, sys.argv) + ret = application.run() + if ret != 0: + application.usage() display_timers() sys.exit(ret) From a603c3a63fb31d27c612b9e1281127fd9c8ac8bf Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 10 Oct 2023 09:51:57 +0200 Subject: [PATCH 057/248] git: Update gitignore. --- .gitignore | 334 ++++++++++++++++++++++++++++++++++++++ doc/dev/documentation.org | 10 +- 2 files changed, 339 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index fcf5e6a1..528fa6d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ ### CUSTOM ### *.exe +*.msi *.tar.xz *.zip *.spec @@ -9,15 +10,33 @@ tmp +*.tar *.tar.* +*.tgz *.zip +*.html +*.pdf +*.odt + +doc/dev/images/auto_* +doc/dev/documentation.* +!doc/dev/*.org + +doc/dev/images/auto_* +doc/users/documentation.* +!doc/users/*.org + +TAGS + pamhyrBuild wine-pamhyr-build* _PAMHYR_ __old__ +*-venv + ### END CUSTOM ### # Created by https://www.toptal.com/developers/gitignore/api/python @@ -309,3 +328,318 @@ compile_commands.json *_qmlcache.qrc # End of https://www.toptal.com/developers/gitignore/api/qt + +# Created by https://www.toptal.com/developers/gitignore/api/latex +# Edit at https://www.toptal.com/developers/gitignore?templates=latex + +### LaTeX ### +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 +.*.lb + +## Intermediate documents: +*.dvi +*.xdv +*-converted-to.* +# these rules might exclude image files for figures etc. +# *.ps +# *.eps +# *.pdf + +## Generated if empty string is given at "Please type another file name for output:" +.pdf + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.run.xml + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex(busy) +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync + +## Build tool directories for auxiliary files +# latexrun +latex.out/ + +## Auxiliary and intermediate files from other packages: +# algorithms +*.alg +*.loa + +# achemso +acs-*.bib + +# amsthm +*.thm + +# beamer +*.nav +*.pre +*.snm +*.vrb + +# changes +*.soc + +# comment +*.cut + +# cprotect +*.cpt + +# elsarticle (documentclass of Elsevier journals) +*.spl + +# endnotes +*.ent + +# fixme +*.lox + +# feynmf/feynmp +*.mf +*.mp +*.t[1-9] +*.t[1-9][0-9] +*.tfm + +#(r)(e)ledmac/(r)(e)ledpar +*.end +*.?end +*.[1-9] +*.[1-9][0-9] +*.[1-9][0-9][0-9] +*.[1-9]R +*.[1-9][0-9]R +*.[1-9][0-9][0-9]R +*.eledsec[1-9] +*.eledsec[1-9]R +*.eledsec[1-9][0-9] +*.eledsec[1-9][0-9]R +*.eledsec[1-9][0-9][0-9] +*.eledsec[1-9][0-9][0-9]R + +# glossaries +*.acn +*.acr +*.glg +*.glo +*.gls +*.glsdefs +*.lzo +*.lzs +*.slg +*.slo +*.sls + +# uncomment this for glossaries-extra (will ignore makeindex's style files!) +# *.ist + +# gnuplot +*.gnuplot +*.table + +# gnuplottex +*-gnuplottex-* + +# gregoriotex +*.gaux +*.glog +*.gtex + +# htlatex +*.4ct +*.4tc +*.idv +*.lg +*.trc +*.xref + +# hyperref +*.brf + +# knitr +*-concordance.tex +# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files +# *.tikz +*-tikzDictionary + +# listings +*.lol + +# luatexja-ruby +*.ltjruby + +# makeidx +*.idx +*.ilg +*.ind + +# minitoc +*.maf +*.mlf +*.mlt +*.mtc[0-9]* +*.slf[0-9]* +*.slt[0-9]* +*.stc[0-9]* + +# minted +_minted* +*.pyg + +# morewrites +*.mw + +# newpax +*.newpax + +# nomencl +*.nlg +*.nlo +*.nls + +# pax +*.pax + +# pdfpcnotes +*.pdfpc + +# sagetex +*.sagetex.sage +*.sagetex.py +*.sagetex.scmd + +# scrwfile +*.wrt + +# svg +svg-inkscape/ + +# sympy +*.sout +*.sympy +sympy-plots-for-*.tex/ + +# pdfcomment +*.upa +*.upb + +# pythontex +*.pytxcode +pythontex-files-*/ + +# tcolorbox +*.listing + +# thmtools +*.loe + +# TikZ & PGF +*.dpth +*.md5 +*.auxlock + +# titletoc +*.ptc + +# todonotes +*.tdo + +# vhistory +*.hst +*.ver + +# easy-todo +*.lod + +# xcolor +*.xcp + +# xmpincl +*.xmpi + +# xindy +*.xdy + +# xypic precompiled matrices and outlines +*.xyc +*.xyd + +# endfloat +*.ttt +*.fff + +# Latexian +TSWLatexianTemp* + +## Editors: +# WinEdt +*.bak +*.sav + +# Texpad +.texpadtmp + +# LyX +*.lyx~ + +# Kile +*.backup + +# gummi +.*.swp + +# KBibTeX +*~[0-9]* + +# TeXnicCenter +*.tps + +# auto folder when using emacs and auctex +./auto/* +*.el + +# expex forward references with \gathertags +*-tags.tex + +# standalone packages +*.sta + +# Makeindex log files +*.lpz + +# xwatermark package +*.xwm + +# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib +# option is specified. Footnotes are the stored in a file with suffix Notes.bib. +# Uncomment the next line to have this generated file ignored. +#*Notes.bib + +### LaTeX Patch ### +# LIPIcs / OASIcs +*.vtc + +# glossaries +*.glstex + +# End of https://www.toptal.com/developers/gitignore/api/latex diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index e50fe58e..bbef8dc2 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -98,7 +98,7 @@ simulation results to model data. #+header: :results drawer #+header: :exports results #+header: :post attr_wrap(width="12cm", data=*this*, name="graph-architecture", caption="Pamhyr2 Model/View architecture scheme (inspired by Qt Model/View architecture [[https://doc.qt.io/qt-5/model-view-programming.html]])", float="t") -#+begin_src dot :file "images/graph-architecture.png" :cache no +#+begin_src dot :file "images/auto_graph-architecture.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=white]; @@ -196,7 +196,7 @@ other components are linked to one of these basic components. #+header: :results drawer #+header: :exports results #+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies (A -> B means A can contain references to B)", float="t") -#+begin_src dot :file "images/graph-model.png" :cache no +#+begin_src dot :file "images/auto_graph-model.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; @@ -717,7 +717,7 @@ execute a code on distant computer, for example, over ssh. #+header: :results drawer #+header: :exports results #+header: :post attr_wrap(width="12cm", data=*this*, name="graph-multi-solver", caption="Scheme of multiple solver configured, one Rubarbe solver and two Mage solver with one on local machine and one on a distant machine accessed over ssh", float="t") -#+begin_src dot :file "images/graph-multi-solver.png" :cache no +#+begin_src dot :file "images/auto_graph-multi-solver.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; @@ -836,7 +836,7 @@ solver and get results: #+header: :results drawer #+header: :exports results #+header: :post attr_wrap(width="10cm", data=*this*, name="graph-pipeline", caption="Pamhyr2 solver execution pipeline architecture scheme", float="t") -#+begin_src dot :file "images/graph-pipeline.png" :cache no +#+begin_src dot :file "images/auto_graph-pipeline.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; @@ -884,7 +884,7 @@ the temporal order of action is prensented in Figure #+header: :results drawer #+header: :exports results #+header: :post attr_wrap(width="12cm", data=*this*, name="graph-pipeline-generic", caption="Pamhyr2 generic solver execution pipeline architecture scheme", float="t") -#+begin_src dot :file "images/graph-pipeline-generic.png" :cache no +#+begin_src dot :file "images/auto_graph-pipeline-generic.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; From ab9ebc6400120bf6286cd0f728823ec0af70209a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 10 Oct 2023 15:17:53 +0200 Subject: [PATCH 058/248] pamhyr: Add just-in-time database geometry loading. --- src/Model/Friction/Friction.py | 4 +- src/Model/Friction/FrictionList.py | 20 +++++++ src/Model/Geometry/PointXYZ.py | 10 +--- src/Model/Geometry/Profile.py | 52 +++++++++++------ src/Model/Geometry/ProfileXYZ.py | 45 +++++++------- src/Model/Geometry/Reach.py | 58 +++++++++++++------ .../InitialConditions/InitialConditions.py | 6 +- .../InitialConditionsDict.py | 14 +++++ src/Model/Tools/PamhyrList.py | 8 +-- 9 files changed, 140 insertions(+), 77 deletions(-) diff --git a/src/Model/Friction/Friction.py b/src/Model/Friction/Friction.py index 2ebd2b4e..7e29e931 100644 --- a/src/Model/Friction/Friction.py +++ b/src/Model/Friction/Friction.py @@ -85,9 +85,7 @@ class Friction(SQLSubModel): sec.begin_strickler = bs sec.end_strickler = es - new[ind] = sec - - return new + yield ind, sec def _sql_save(self, execute, data = None): ind = data["ind"] diff --git a/src/Model/Friction/FrictionList.py b/src/Model/Friction/FrictionList.py index 215f96dd..777729c7 100644 --- a/src/Model/Friction/FrictionList.py +++ b/src/Model/Friction/FrictionList.py @@ -68,6 +68,26 @@ class FrictionList(PamhyrModelList): return ok + def _get_frictions_list(self): + # Frictions list generator is type (int, Point) with the first + # element the index of the Point in list + return list( + map( + lambda p: p[1], + sorted( + self._lst, + key = lambda p: p[0] + ) + ) + ) + + @property + def lst(self): + if not isinstance(self._lst, list): + self._lst = self._get_frictions_list() + + return self._lst + @property def frictions(self): return self.lst diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index a8f19c8b..6f1b5784 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -70,8 +70,6 @@ class PointXYZ(Point, SQLSubModel): @classmethod def _sql_load(cls, execute, data = None): - points = [] - status = data["status"] profile = data["profile"] @@ -81,10 +79,6 @@ class PointXYZ(Point, SQLSubModel): f"WHERE profile = {profile.id}" ) - # Create points list - for _ in table: - points.append(None) - # Fill points list with new point for row in table: ind = row[0] @@ -111,9 +105,7 @@ class PointXYZ(Point, SQLSubModel): ) ) - points[ind] = new - - return points + yield ind, new def _sql_save(self, execute, data = None): profile = data["profile"] diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 606b2e29..6290498c 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -57,18 +57,34 @@ class Profile(object): self._profile_type = _type def __len__(self): - return len(self._points) + return len(self.points) @property def number_points(self): - return len(self._points) + return len(self.points) + + def _get_points_list(self): + # Points list generator is type (int, Point) with the first + # element the index of the Point in list + return list( + map( + lambda p: p[1], + sorted( + self._points, + key = lambda p: p[0] + ) + ) + ) @property def points(self): - return self._points.copy() + if not isinstance(self._points, list): + self._points = self._get_points_list() + + return self._points def point(self, index): - return self._points[index] + return self.points[index] @property def reach(self): @@ -115,7 +131,7 @@ class Profile(object): @property def nb_points(self): - return len(self._points) + return len(self.points) @property def kp(self): @@ -170,8 +186,8 @@ class Profile(object): self._status.modified() def point(self, i:int): - if i < len(self._points): - return self._points[i] + if i < len(self.points): + return self.points[i] return None @@ -181,7 +197,7 @@ class Profile(object): Returns: The list of named point """ - return [point for point in self._points + return [point for point in self.points if point.point_is_named()] @@ -195,7 +211,7 @@ class Profile(object): Returns: Nothing. """ - self._points.insert(index, point) + self.points.insert(index, point) self._status.modified() @@ -218,7 +234,7 @@ class Profile(object): ) ) - self._points = list( + self.points = list( filter( lambda p: p not in points, self.points @@ -235,7 +251,7 @@ class Profile(object): Returns: Nothing. """ - self._points = list( + self.points = list( filter( lambda p: p not in points, self.points @@ -246,10 +262,10 @@ class Profile(object): # Move def move_up_point(self, index: int): - if index < len(self._points): + if index < len(self.points): next = index - 1 - p = self._points + p = self.points p[index], p[next] = p[next], p[index] self._status.modified() @@ -257,7 +273,7 @@ class Profile(object): if index >= 0: prev = index + 1 - p = self._points + p = self.points p[index], p[prev] = p[prev], p[index] self._status.modified() @@ -271,8 +287,8 @@ class Profile(object): elif column == 'z': predicate = lambda p: p.z - self._points = sorted( - self._points, + self.points = sorted( + self.points, key=predicate, reverse=is_reversed ) @@ -280,10 +296,10 @@ class Profile(object): @timer def sort_with_indexes(self, indexes: list): - if len(self._points) != len(indexes): + 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 0652d1f3..7eb12ac5 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -16,6 +16,7 @@ # -*- coding: utf-8 -*- +import logging import numpy as np from typing import List @@ -27,6 +28,8 @@ from Model.Geometry.Profile import Profile from Model.Geometry.PointXYZ import PointXYZ from Model.Geometry.Vector_1d import Vector1d +logger = logging.getLogger() + class ProfileXYZ(Profile, SQLSubModel): _sub_classes = [ PointXYZ, @@ -108,7 +111,7 @@ class ProfileXYZ(Profile, SQLSubModel): table = execute( "SELECT id, ind, name, kp, num, code1, code2, sl " + "FROM geometry_profileXYZ " + - f"WHERE reach = {reach}" + f"WHERE reach = {reach.id}" ) for _ in table: @@ -145,9 +148,11 @@ class ProfileXYZ(Profile, SQLSubModel): data["profile"] = new new._points = PointXYZ._sql_load(execute, data) - profiles[ind] = new + yield ind, new - return profiles + # profiles[ind] = new + + # return profiles def _sql_save(self, execute, data = None): ok = True @@ -170,7 +175,7 @@ class ProfileXYZ(Profile, SQLSubModel): execute(f"DELETE FROM geometry_pointXYZ WHERE profile = {self.id}") ind = 0 - for point in self._points: + for point in self.points: data["ind"] = ind ok &= point._sql_save(execute, data) ind += 1 @@ -222,16 +227,16 @@ class ProfileXYZ(Profile, SQLSubModel): def x(self): - return [point.x for point in self._points] + return [point.x for point in self.points] def y(self): - return [point.y for point in self._points] + return [point.y for point in self.points] def z(self): - return [point.z for point in self._points] + return [point.z for point in self.points] def names(self): - return [point.name for point in self._points] + return [point.name for point in self.points] def x_max(self): return max(self.filter_isnan(self.x())) @@ -262,7 +267,7 @@ class ProfileXYZ(Profile, SQLSubModel): """ for point in list_points: pt = PointXYZ(*point, profile=self, status=self._status) - self._points.append(pt) + self.points.append(pt) self._status.modified() def get_point_i(self, index: int) -> PointXYZ: @@ -275,7 +280,7 @@ class ProfileXYZ(Profile, SQLSubModel): The point. """ try: - return self._points[index] + return self.points[index] except IndexError: raise IndexError(f"Invalid point index: {index}") @@ -286,7 +291,7 @@ class ProfileXYZ(Profile, SQLSubModel): Nothing. """ point_xyz = PointXYZ(0., 0., 0., profile=self, status=self._status) - self._points.append(point_xyz) + self.points.append(point_xyz) self._status.modified() def insert(self, index: int): @@ -299,7 +304,7 @@ class ProfileXYZ(Profile, SQLSubModel): The new point. """ point = PointXYZ(0., 0., 0., profile=self, status=self._status) - self._points.insert(index, point) + self.points.insert(index, point) self._status.modified() return point @@ -315,9 +320,9 @@ class ProfileXYZ(Profile, SQLSubModel): return [x for x in lst if not np.isnan(x)] def _first_point_not_nan(self): - first_point = self._points[0] + first_point = self.points[0] - for point in self._points: + for point in self.points: if not point.is_nan(): first_point = point break @@ -325,9 +330,9 @@ class ProfileXYZ(Profile, SQLSubModel): return first_point def _last_point_not_nan(self): - last_point = self._points[-1] + last_point = self.points[-1] - for point in self._points[::-1]: + for point in self.points[::-1]: if not point.is_nan(): last_point = point break @@ -354,13 +359,13 @@ class ProfileXYZ(Profile, SQLSubModel): first_point_not_nan = self._first_point_not_nan() last_point_not_nan = self._last_point_not_nan() - for index, point in enumerate(self._points): + for index, point in enumerate(self.points): if point.point_is_named(): index_first_named_point = index first_named_point = point break - for point in reversed(self._points): + for point in reversed(self.points): if point.point_is_named(): last_named_point = point break @@ -378,7 +383,7 @@ class ProfileXYZ(Profile, SQLSubModel): vector = Vector1d(first_point_not_nan, last_point_not_nan) normalized_direction_vec = vector.normalized_direction_vector() - for point in self._points: + for point in self.points: xi = point.x - first_named_point.x yi = point.y - first_named_point.y station_i = (normalized_direction_vec[0] * xi + @@ -390,7 +395,7 @@ class ProfileXYZ(Profile, SQLSubModel): vector = Vector1d(first_point_not_nan, last_point_not_nan) normalized_direction_vec = vector.normalized_direction_vector() - for point in self._points: + for point in self.points: xi = point.x - first_point_not_nan.x yi = point.y - first_point_not_nan.y station_i = (normalized_direction_vec[0] * xi + diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 36fca25f..4fdc8c74 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -94,8 +94,8 @@ class Reach(SQLSubModel): Returns: The profile at index i """ - if i < len(self._profiles): - return self._profiles[i] + if i < len(self.profiles): + return self.profiles[i] return None @@ -106,12 +106,32 @@ class Reach(SQLSubModel): return self._parent.name + def _get_profiles_list(self): + # Profiles list generator is type (int, Point) with the first + # element the index of the Point in list + logger.info(f"Load profiles from reach {self.name}") + return list( + map( + lambda p: p[1], + sorted( + self._profiles, + key = lambda p: p[0] + ) + ) + ) + def __len__(self): + if not isinstance(self._profiles, list): + self._profiles = self._get_profiles_list() + return len(self._profiles) @property def profiles(self): - return self._profiles.copy() + if not isinstance(self._profiles, list): + self._profiles = self._get_profiles_list() + + return self._profiles def get_profiles_from_kp(self, kp): return list( @@ -126,14 +146,14 @@ class Reach(SQLSubModel): Returns: Number of profiles """ - return len(self._profiles) + return len(self.profiles) def get_geometry(self) -> List[Profile]: """ Returns: The profiles list. """ - return self._profiles + return self.profiles def _update_profile_numbers(self): """Update profiles index @@ -155,7 +175,7 @@ class Reach(SQLSubModel): """ profile = ProfileXYZ(reach=self, status=self._status) - self._profiles.insert(index, profile) + self.profiles.insert(index, profile) self._update_profile_numbers() self._status.modified() @@ -171,7 +191,7 @@ class Reach(SQLSubModel): Returns: Nothing. """ - self._profiles.insert(index, profile) + self.profiles.insert(index, profile) self._update_profile_numbers() self._status.modified() @@ -195,7 +215,7 @@ class Reach(SQLSubModel): ) ) - self._profiles = list( + self.profiles = list( filter( lambda p: p not in profiles, self.profiles @@ -213,7 +233,7 @@ class Reach(SQLSubModel): Returns: Nothing. """ - self._profiles = list( + self.profiles = list( filter( lambda p: p not in profiles, self.profiles @@ -227,7 +247,7 @@ class Reach(SQLSubModel): if index < len(self.profiles): next = index - 1 - p = self._profiles + p = self.profiles p[index], p[next] = p[next], p[index] self._status.modified() @@ -235,7 +255,7 @@ class Reach(SQLSubModel): if index >= 0: prev = index + 1 - p = self._profiles + p = self.profiles p[index], p[prev] = p[prev], p[index] self._status.modified() @@ -347,7 +367,7 @@ class Reach(SQLSubModel): Tuple of complete and incomplete guidelines name. """ # Get all point contains into a guideline - named_points = [profile.named_points() for profile in self._profiles] + named_points = [profile.named_points() for profile in self.profiles] points_name = list( map( lambda lst: list(map(lambda p: p.name, lst)), @@ -422,8 +442,8 @@ class Reach(SQLSubModel): @timer def sort(self, is_reversed: bool = False): - self._profiles = sorted( - self._profiles, + self.profiles = sorted( + self.profiles, key=lambda profile: profile.kp, reverse=is_reversed ) @@ -431,10 +451,10 @@ class Reach(SQLSubModel): @timer def sort_with_indexes(self, indexes: list): - if len(self._profiles) != len(indexes): + if len(self.profiles) != len(indexes): logger.critical("Indexes list do not correspond to profile list") - self._profiles = list( + self.profiles = list( map( lambda x: x[1], sorted( @@ -474,7 +494,7 @@ class Reach(SQLSubModel): **d, reach=self, status=self._status ) prof.import_points(profile) - self._profiles.append(prof) + self.profiles.append(prof) self._update_profile_numbers() self._status.modified() @@ -538,8 +558,8 @@ class Reach(SQLSubModel): # TODO: Move this function to model reach def export_reach(self, filename): with open(f"{filename}", "w") as file_st: - for index in range(len(self._profiles)): - profile = self._profiles[index] + for index in range(len(self.profiles)): + profile = self.profiles[index] for v in profile.header: file_st.write(f"{v:>6}") diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py index 2c50d10c..a40e0261 100644 --- a/src/Model/InitialConditions/InitialConditions.py +++ b/src/Model/InitialConditions/InitialConditions.py @@ -244,10 +244,8 @@ class InitialConditions(SQLSubModel): data = data ) - if new._data is None: - return None - - return new + if new._data is not None: + yield new def _sql_save(self, execute, data = None): ok = True diff --git a/src/Model/InitialConditions/InitialConditionsDict.py b/src/Model/InitialConditions/InitialConditionsDict.py index 40fdca49..f2bd038f 100644 --- a/src/Model/InitialConditions/InitialConditionsDict.py +++ b/src/Model/InitialConditions/InitialConditionsDict.py @@ -16,6 +16,7 @@ # -*- coding: utf-8 -*- +import types from copy import copy from tools import trace, timer @@ -57,6 +58,19 @@ class InitialConditionsDict(PamhyrModelDict): return ok + + def get(self, key): + if key in self._dict: + v = self._dict[key] + if isinstance(v, types.GeneratorType): + self._dict[key] = list(v)[0] + + return self._dict[key] + + new = self.new(key) + self.set(key, new) + return new + def new(self, reach): new = InitialConditions(reach = reach, status = self._status) self.set(reach, new) diff --git a/src/Model/Tools/PamhyrList.py b/src/Model/Tools/PamhyrList.py index 6938343f..f1896c90 100644 --- a/src/Model/Tools/PamhyrList.py +++ b/src/Model/Tools/PamhyrList.py @@ -59,12 +59,12 @@ class PamhyrModelList(SQLSubModel): # MODEL METHOD # ################ - def __len__(self): - return len(self._lst) - @property def lst(self): - return self._lst.copy() + return self._lst + + def __len__(self): + return len(self.lst) def get(self, row): return self._lst[row] From acfb876da0cdfb7337f7474f078274cf15174da3 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 10 Oct 2023 15:28:59 +0200 Subject: [PATCH 059/248] doc: Change the Firefox button to an open button. --- src/View/Doc/Window.py | 23 +++++++++-------------- src/View/ui/WebView.ui | 9 ++++++--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py index 4be981b8..e197e566 100644 --- a/src/View/Doc/Window.py +++ b/src/View/Doc/Window.py @@ -20,18 +20,16 @@ import os, sys import logging import subprocess +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QAction +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings + from View.Tools.PamhyrWindow import PamhyrWindow -from PyQt5.QtCore import QCoreApplication - -_translate = QCoreApplication.translate logger = logging.getLogger() -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QAction -from PyQt5.QtCore import QUrl -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings - class DocWindow(PamhyrWindow): _pamhyr_ui = "WebView" _pamhyr_name = "Doc" @@ -62,7 +60,6 @@ class DocWindow(PamhyrWindow): config = config, options = [], parent = parent - ) self._path = self._path_file(filename) @@ -75,7 +72,7 @@ class DocWindow(PamhyrWindow): self.findChild(QAction, "action_back").triggered.connect(self.back) self.findChild(QAction, "action_forward").triggered.connect(self.forward) - self.findChild(QAction, "action_firefox").triggered.connect(self.open_in_firefox) + self.findChild(QAction, "action_open").triggered.connect(self.open) def setup_web_engine(self): @@ -99,8 +96,6 @@ class DocWindow(PamhyrWindow): def forward(self): self._web_view.forward() - def open_in_firefox(self): - _ = subprocess.Popen( - f"firefox {self._path}", - shell = True - ) + def open(self): + url = QUrl(f"file://{self._path}") + QDesktopServices.openUrl(url) diff --git a/src/View/ui/WebView.ui b/src/View/ui/WebView.ui index 7d0a41bf..225c5948 100644 --- a/src/View/ui/WebView.ui +++ b/src/View/ui/WebView.ui @@ -13,6 +13,9 @@ MainWindow + + + @@ -43,11 +46,11 @@ - + - + - Firefox + Open Open document in Firefox From 5b2ee803291441164f951d8b7fcdd1be598d6c25 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 11 Oct 2023 08:37:20 +0200 Subject: [PATCH 060/248] geometry, results, configure: Some fixes. --- src/Checker/Checker.py | 6 +++ src/Model/Geometry/ProfileXYZ.py | 2 +- src/Model/Geometry/Reach.py | 4 +- src/Model/Stricklers/StricklersList.py | 4 +- src/Model/Tools/PamhyrList.py | 23 +++++++--- src/View/CheckList/Worker.py | 2 +- src/View/Configure/Translate.py | 33 ++++++++++++++ src/View/Configure/Window.py | 63 +++++++++----------------- src/View/Tools/PamhyrTable.py | 2 + 9 files changed, 86 insertions(+), 53 deletions(-) create mode 100644 src/View/Configure/Translate.py diff --git a/src/Checker/Checker.py b/src/Checker/Checker.py index d10a8c4d..762db1c6 100644 --- a/src/Checker/Checker.py +++ b/src/Checker/Checker.py @@ -62,6 +62,12 @@ class AbstractModelChecker(object): # Abstract function + def _run(self, study): + from Model.Study import Study + + thread_study = Study.open(study._filename) + self.run(thread_study) + def run(self, study): """Run checker function diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 7eb12ac5..929928b4 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -146,7 +146,7 @@ class ProfileXYZ(Profile, SQLSubModel): ) data["profile"] = new - new._points = PointXYZ._sql_load(execute, data) + new._points = PointXYZ._sql_load(execute, data.copy()) yield ind, new diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 4fdc8c74..a6ed514c 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -78,7 +78,7 @@ class Reach(SQLSubModel): data = {} ind = 0 - for profile in self._profiles: + for profile in self.profiles: data["ind"] = ind profile._sql_save(execute, data) ind += 1 @@ -136,7 +136,7 @@ class Reach(SQLSubModel): def get_profiles_from_kp(self, kp): return list( filter( - lambda p: p.kp == kp, self._profiles + lambda p: p.kp == kp, self.profiles ) ) diff --git a/src/Model/Stricklers/StricklersList.py b/src/Model/Stricklers/StricklersList.py index b000c07e..f1d36fa7 100644 --- a/src/Model/Stricklers/StricklersList.py +++ b/src/Model/Stricklers/StricklersList.py @@ -72,4 +72,6 @@ class StricklersList(PamhyrModelList): key = f, reverse = reverse, ) - self._status.modified() + + if self._status is not None: + self._status.modified() diff --git a/src/Model/Tools/PamhyrList.py b/src/Model/Tools/PamhyrList.py index f1896c90..9a83bdb0 100644 --- a/src/Model/Tools/PamhyrList.py +++ b/src/Model/Tools/PamhyrList.py @@ -71,7 +71,8 @@ class PamhyrModelList(SQLSubModel): def set(self, row, new): self._lst[row] = new - self._status.modified() + if self._status is not None: + self._status.modified() def new(self, index): """Create new elements and add it to list @@ -86,12 +87,16 @@ class PamhyrModelList(SQLSubModel): def insert(self, index, new): self._lst.insert(index, new) - self._status.modified() + + if self._status is not None: + self._status.modified() def delete(self, lst): for el in lst: self._lst.remove(el) - self._status.modified() + + if self._status is not None: + self._status.modified() def delete_i(self, indexes): lst = list( @@ -107,7 +112,9 @@ class PamhyrModelList(SQLSubModel): def sort(self, reverse=False, key=None): self._lst.sort(reverse=reverse, key=key) - self._status.modified() + + if self._status is not None: + self._status.modified() def move_up(self, index): if index < len(self._lst): @@ -115,7 +122,9 @@ class PamhyrModelList(SQLSubModel): l = self._lst l[index], l[next] = l[next], l[index] - self._status.modified() + + if self._status is not None: + self._status.modified() def move_down(self, index): if index >= 0: @@ -123,7 +132,9 @@ class PamhyrModelList(SQLSubModel): l = self._lst l[index], l[prev] = l[prev], l[index] - self._status.modified() + + if self._status is not None: + self._status.modified() class PamhyrModelListWithTab(SQLSubModel): diff --git a/src/View/CheckList/Worker.py b/src/View/CheckList/Worker.py index 66e34d2c..aacd9393 100644 --- a/src/View/CheckList/Worker.py +++ b/src/View/CheckList/Worker.py @@ -42,6 +42,6 @@ class Worker(QObject): self.signalStatus.emit(checker.name) # Run checker - checker.run(self._study) + checker._run(self._study) self.signalStatus.emit("progress") diff --git a/src/View/Configure/Translate.py b/src/View/Configure/Translate.py new file mode 100644 index 00000000..bed6a713 --- /dev/null +++ b/src/View/Configure/Translate.py @@ -0,0 +1,33 @@ +# 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 . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + +class ConfigureTranslate(PamhyrTranslate): + def __init__(self): + super(ConfigureTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("Configure", "Name"), + "type": _translate("Configure", "Type"), + "description": _translate("Configure", "Description"), + } diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index 83db81f6..b07304fa 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -22,11 +22,13 @@ from copy import deepcopy from config import Config from View.Tools.PamhyrWindow import PamhyrDialog +from View.Tools.PamhyrTable import PamhyrTableModel from View.Stricklers.Table import TableModel -from View.Stricklers.translate import * +from View.Stricklers.translate import StricklersTranslate from View.Stricklers.UndoCommand import * +from View.Configure.Translate import ConfigureTranslate from View.Configure.Solver.Window import ConfigureSolverWindow from PyQt5.QtGui import ( @@ -47,50 +49,28 @@ from Solver.Solvers import solver_long_name logger = logging.getLogger() -class SolverTableModel(QAbstractTableModel): - def __init__(self, headers=[], rows=[]): - super(QAbstractTableModel, self).__init__() - self.rows = rows - self.headers = headers - - def rowCount(self, parent): - return len(self.rows) - - def columnCount(self, parent): - return len(self.headers) - +class SolverTableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() - ret = self.rows[index.row()][self.headers[index.column()]] + ret = self._data[index.row()][self._headers[index.column()]] - if self.headers[index.column()] == "type": + if self._headers[index.column()] == "type": ret = solver_long_name[ret] return ret - def headerData(self, section, orientation, role): - if (role == Qt.ItemDataRole.DisplayRole and - orientation == Qt.Orientation.Horizontal): - return self.headers[section].capitalize() - - if (role == Qt.ItemDataRole.DisplayRole and - orientation == Qt.Orientation.Vertical): - return section + 1 # Start at 1 - - return QVariant() - def removeRow(self, index): - del self.rows[index.row()] + del self._data[index.row()] self.layoutChanged.emit() def add_solver(self, solver): - self.rows.append(solver) + self._data.append(solver) self.layoutChanged.emit() def change_solver(self, solver, index): - self.rows[index.row()] = solver + self._data[index.row()] = solver self.layoutChanged.emit() @@ -105,10 +85,13 @@ class ConfigureWindow(PamhyrDialog): super(ConfigureWindow, self).__init__( title = self._pamhyr_name, config = config, + trad = ConfigureTranslate(), options = [], parent=parent ) + self._trad_stricklers = StricklersTranslate() + self.setup_custom_sc() self.setup_solver() self.setup_stricklers() @@ -118,26 +101,21 @@ class ConfigureWindow(PamhyrDialog): def setup_solver(self): table = self.find(QTableView, "tableView_solver") self.solver_table_model = SolverTableModel( - headers = ["name", "type", "description"], - rows = self._config.solvers + table_view = table, + table_headers = self._trad.get_dict("table_headers"), + data = self._config.solvers ) - table.setModel(self.solver_table_model) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) - table.resizeColumnsToContents() def setup_stricklers(self): table = self.find(QTableView, f"tableView_stricklers") self._stricklers = deepcopy(self._config.stricklers) self._stricklers_table = TableModel( + table_view = table, + table_headers = self._trad_stricklers.get_dict("table_headers"), + editable_headers = ["name", "comment", "minor", "medium"], data = self._stricklers, undo = self._undo_stack, ) - table.setModel(self._stricklers_table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) def setup_data(self): # Meshing_Tool @@ -197,7 +175,7 @@ class ConfigureWindow(PamhyrDialog): def accept(self): # Solvers - self._config.solvers = self.solver_table_model.rows.copy() + self._config.solvers = self.solver_table_model._data.copy() # Meshing_Tool self._config.meshing_tool = self.get_line_edit_text("lineEdit_meshing_tool") @@ -232,6 +210,7 @@ class ConfigureWindow(PamhyrDialog): self.close() # Debug + def set_debug(self): self._config.debug = not self._config.debug logger.info(f"Debug mode set : {self._config.debug}") @@ -243,7 +222,7 @@ class ConfigureWindow(PamhyrDialog): indexes = self.find(QTableView, "tableView_solver").selectionModel().selectedRows() for index in indexes: self.edit_solver = ConfigureSolverWindow( - data=self.solver_table_model.rows[index.row()], + data=self.solver_table_model._data[index.row()], config=self._config, parent=self ) diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index d809dced..43aa22c2 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -110,6 +110,8 @@ class PamhyrTableModel(QAbstractTableModel): self._table_view.setSelectionBehavior(QAbstractItemView.SelectRows) self._table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self._table_view.setAlternatingRowColors(True) + self._table_view.resizeColumnsToContents() + def flags(self, index): column = index.column() From 1f8b0bddb4e1e2d53571b388d8188c91d68a33ea Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 11 Oct 2023 08:52:05 +0200 Subject: [PATCH 061/248] ui: Fix geometry cross section window lang. --- src/View/ui/GeometryCrossSection.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/View/ui/GeometryCrossSection.ui b/src/View/ui/GeometryCrossSection.ui index 67e06ced..6e6d62a5 100644 --- a/src/View/ui/GeometryCrossSection.ui +++ b/src/View/ui/GeometryCrossSection.ui @@ -13,6 +13,9 @@ MainWindow + + + From 8544dd11443c493b7482567d98799164ab7f67ad Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 11 Oct 2023 10:29:15 +0200 Subject: [PATCH 062/248] doc: Disactive documentation display in Pamhyr2 for lighter packages. --- requirements.txt | 2 +- src/View/MainWindow.py | 49 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 01d9a29a..2f9bcc4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ PyQt5==5.15.4 PyQt5-Qt5==5.15.2 PyQt5-sip==12.12.2 -PyQtWebEngine==5.15.6 +#PyQtWebEngine==5.15.6 pyqtgraph>=0.12.1 matplotlib>=3.4.1 numpy>=1.24.2 diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 8539af1d..cf7e08a7 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -16,7 +16,7 @@ # -*- coding: utf-8 -*- -import os +import os, sys import logging import subprocess from queue import Queue @@ -56,9 +56,17 @@ from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow from View.Results.Window import ResultsWindow -from View.Doc.Window import DocWindow from View.Debug.Window import ReplWindow +# Optional internal display of documentation for make the application +# package lighter... +try: + from View.Doc.Window import DocWindow + _doc = "internal" +except Exception as e: + print("Handle exception: {e}") + _doc = "external" + from Model.Study import Study logger = logging.getLogger() @@ -132,6 +140,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self.conf.last_study != "" and not self.conf.close_correctly: self.dialog_reopen_study() + if _doc == "external": + logger.info("doc: Internal documentation is not available...") + def set_title(self): title = "(dbg) " if self.conf.debug else "" @@ -726,16 +737,36 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # DOCUMENTATION # ################# + def _doc_path_file(self, filename): + if ".py" in sys.argv[0]: + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "doc", filename + ) + ) + + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "doc", filename + ) + ) + def open_doc(self, filename): - if ".odt" in filename: - url = QUrl(f"file://{DocWindow._path_file(filename)}") + if _doc == "external": + url = QUrl(f"file://{self._doc_path_file(filename)}") QDesktopServices.openUrl(url) else: - doc = DocWindow( - filename = filename, - parent = self - ) - doc.show() + if ".odt" in filename: + url = QUrl(f"file://{DocWindow._path_file(filename)}") + QDesktopServices.openUrl(url) + else: + doc = DocWindow( + filename = filename, + parent = self + ) + doc.show() def open_doc_user(self, ext="pdf"): self.open_doc(f"Pamhyr2-users.{ext}") From fa2454d0795b87ac7cc505c03df0c46c3ab71efc Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 11 Oct 2023 11:06:45 +0200 Subject: [PATCH 063/248] pep8: Some modifications to respect pep8 standard code. --- src/Checker/Checker.py | 2 + src/Checker/Mage.py | 9 +- src/Checker/Study.py | 8 +- .../BoundaryCondition/BoundaryCondition.py | 36 ++--- .../BoundaryConditionList.py | 9 +- .../BoundaryConditionTypes.py | 22 +-- src/Model/Except.py | 10 +- src/Model/Friction/Friction.py | 11 +- src/Model/Friction/FrictionList.py | 13 +- src/Model/Geometry/Point.py | 4 +- src/Model/Geometry/PointXY.py | 7 +- src/Model/Geometry/PointXYZ.py | 24 ++-- src/Model/Geometry/Profile.py | 25 ++-- src/Model/Geometry/ProfileXYZ.py | 44 +++--- src/Model/Geometry/Reach.py | 18 ++- src/Model/Geometry/Vector_1d.py | 1 + .../InitialConditions/InitialConditions.py | 77 +++++------ .../InitialConditionsDict.py | 12 +- .../LateralContribution.py | 38 ++--- .../LateralContributionList.py | 9 +- .../LateralContributionTypes.py | 11 +- src/Model/Network/Edge.py | 9 +- src/Model/Network/Graph.py | 30 ++-- src/Model/Network/Node.py | 8 +- src/Model/Network/Point.py | 2 +- src/Model/Results/Results.py | 3 +- src/Model/Results/River/River.py | 9 +- src/Model/River.py | 56 ++++---- src/Model/Saved.py | 1 + src/Model/SedimentLayer/SedimentLayer.py | 47 +++---- src/Model/SedimentLayer/SedimentLayerList.py | 9 +- src/Model/Serializable.py | 1 + .../SolverParameters/SolverParametersList.py | 60 ++++---- src/Model/Stricklers/Stricklers.py | 30 ++-- src/Model/Stricklers/StricklersList.py | 19 +-- src/Model/Study.py | 64 +++++---- src/Model/Tools/PamhyrDB.py | 39 +++--- src/Model/Tools/PamhyrDict.py | 6 +- src/Model/Tools/PamhyrList.py | 13 +- src/Scripts/AScript.py | 1 + src/Scripts/P3DST.py | 3 +- src/Solver/ASolver.py | 17 ++- src/Solver/GenericSolver.py | 1 + src/Solver/Mage.py | 83 ++++++----- src/View/About/Window.py | 13 +- src/View/BoundaryCondition/Edit/Plot.py | 20 +-- src/View/BoundaryCondition/Edit/Table.py | 1 + .../BoundaryCondition/Edit/UndoCommand.py | 4 + src/View/BoundaryCondition/Edit/Window.py | 45 +++--- src/View/BoundaryCondition/Edit/translate.py | 1 + src/View/BoundaryCondition/Table.py | 12 +- src/View/BoundaryCondition/UndoCommand.py | 6 + src/View/BoundaryCondition/Window.py | 45 +++--- src/View/BoundaryCondition/translate.py | 1 + src/View/CheckList/Translate.py | 1 + src/View/CheckList/Window.py | 28 ++-- src/View/CheckList/Worker.py | 1 + src/View/Configure/Solver/Window.py | 28 ++-- src/View/Configure/Translate.py | 1 + src/View/Configure/Window.py | 74 ++++++---- src/View/Debug/Window.py | 17 ++- src/View/Doc/Window.py | 21 +-- src/View/DummyWindow.py | 5 +- src/View/Frictions/PlotStricklers.py | 6 +- src/View/Frictions/Table.py | 1 + src/View/Frictions/UndoCommand.py | 7 + src/View/Frictions/Window.py | 72 +++++----- src/View/Frictions/translate.py | 1 + src/View/Geometry/PlotAC.py | 39 ++++-- src/View/Geometry/PlotKPZ.py | 15 +- src/View/Geometry/PlotXY.py | 17 +-- src/View/Geometry/Profile/Plot.py | 13 +- src/View/Geometry/Profile/Translate.py | 1 + src/View/Geometry/Profile/UndoCommand.py | 9 +- src/View/Geometry/Profile/Window.py | 44 +++--- src/View/Geometry/Table.py | 10 +- src/View/Geometry/Translate.py | 7 +- src/View/Geometry/UndoCommand.py | 7 +- src/View/Geometry/Window.py | 75 +++++----- src/View/InitialConditions/DialogDischarge.py | 7 +- src/View/InitialConditions/DialogHeight.py | 7 +- src/View/InitialConditions/PlotDKP.py | 3 +- src/View/InitialConditions/PlotDischarge.py | 3 +- src/View/InitialConditions/Table.py | 1 + src/View/InitialConditions/UndoCommand.py | 5 + src/View/InitialConditions/Window.py | 51 +++---- src/View/InitialConditions/translate.py | 7 +- src/View/LateralContribution/Edit/Plot.py | 20 +-- .../LateralContribution/Edit/UndoCommand.py | 4 + src/View/LateralContribution/Edit/Window.py | 46 +++---- .../LateralContribution/Edit/translate.py | 1 + src/View/LateralContribution/Table.py | 6 +- src/View/LateralContribution/UndoCommand.py | 7 + src/View/LateralContribution/Window.py | 55 ++++---- src/View/LateralContribution/translate.py | 1 + src/View/MainWindow.py | 130 +++++++++--------- src/View/Network/GraphWidget.py | 17 ++- src/View/Network/Table.py | 4 +- src/View/Network/UndoCommand.py | 11 +- src/View/Network/Window.py | 43 +++--- src/View/Network/translate.py | 1 + src/View/Results/PlotAC.py | 13 +- src/View/Results/PlotH.py | 19 +-- src/View/Results/PlotKPC.py | 13 +- src/View/Results/PlotSedProfile.py | 18 +-- src/View/Results/PlotSedReach.py | 11 +- src/View/Results/PlotXY.py | 19 +-- src/View/Results/Window.py | 100 +++++++------- src/View/Results/translate.py | 1 + src/View/RunSolver/Log/Window.py | 16 ++- src/View/RunSolver/Window.py | 50 +++---- src/View/SedimentLayers/Edit/Plot.py | 20 +-- src/View/SedimentLayers/Edit/UndoCommand.py | 9 ++ src/View/SedimentLayers/Edit/Window.py | 39 +++--- src/View/SedimentLayers/Edit/translate.py | 1 + src/View/SedimentLayers/Reach/Plot.py | 13 +- src/View/SedimentLayers/Reach/Profile/Plot.py | 13 +- .../SedimentLayers/Reach/Profile/Table.py | 1 + .../Reach/Profile/UndoCommand.py | 1 + .../SedimentLayers/Reach/Profile/Window.py | 39 +++--- .../SedimentLayers/Reach/Profile/translate.py | 1 + src/View/SedimentLayers/Reach/SLDialog.py | 7 +- src/View/SedimentLayers/Reach/Table.py | 1 + src/View/SedimentLayers/Reach/UndoCommand.py | 1 + src/View/SedimentLayers/Reach/Window.py | 58 ++++---- src/View/SedimentLayers/Reach/translate.py | 1 + src/View/SedimentLayers/UndoCommand.py | 5 + src/View/SedimentLayers/Window.py | 41 +++--- src/View/SedimentLayers/translate.py | 1 + src/View/SolverParameters/Table.py | 1 + src/View/SolverParameters/UndoCommand.py | 1 + src/View/SolverParameters/Window.py | 27 ++-- src/View/SolverParameters/translate.py | 1 + src/View/Stricklers/Table.py | 1 + src/View/Stricklers/UndoCommand.py | 7 + src/View/Stricklers/Window.py | 22 +-- src/View/Study/Window.py | 17 +-- src/View/Tools/ASubWindow.py | 58 ++++---- src/View/Tools/ListedSubWindow.py | 6 +- src/View/Tools/PamhyrDelegate.py | 1 + src/View/Tools/PamhyrPlot.py | 3 +- src/View/Tools/PamhyrTable.py | 20 +-- src/View/Tools/PamhyrWidget.py | 6 +- src/View/Tools/PamhyrWindow.py | 31 +++-- src/View/Tools/Plot/APlot.py | 5 +- src/View/Tools/Plot/OnPickEvent.py | 13 +- src/View/Tools/Plot/PamhyrCanvas.py | 1 + src/View/Tools/Plot/PamhyrToolbar.py | 34 +++-- src/config.py | 51 ++++--- src/init.py | 26 ++-- src/pamhyr.py | 20 ++- src/tools.py | 60 +++++--- 152 files changed, 1626 insertions(+), 1277 deletions(-) diff --git a/src/Checker/Checker.py b/src/Checker/Checker.py index 762db1c6..e97e0b04 100644 --- a/src/Checker/Checker.py +++ b/src/Checker/Checker.py @@ -20,12 +20,14 @@ from enum import Enum from Model.Except import NotImplementedMethodeError + class STATUS(Enum): UNKNOWN = -1 OK = 0 WARNING = 1 ERROR = 2 + class AbstractModelChecker(object): def __init__(self): self._name = "" diff --git a/src/Checker/Mage.py b/src/Checker/Mage.py index edb0c349..d5b527fa 100644 --- a/src/Checker/Mage.py +++ b/src/Checker/Mage.py @@ -27,8 +27,9 @@ from Checker.Checker import AbstractModelChecker, STATUS _translate = QCoreApplication.translate + class MageNetworkGraphChecker(AbstractModelChecker): - def __init__(self, connectivity = True): + def __init__(self, connectivity=True): super(MageNetworkGraphChecker, self).__init__() self._mode_conn = connectivity @@ -38,8 +39,10 @@ class MageNetworkGraphChecker(AbstractModelChecker): else: mode = "cycle" - self._name = _translate("Checker", f"Mage network graph {mode} checker") - self._description = _translate("Checker", "Check if the network graph is valid") + self._name = _translate( + "Checker", f"Mage network graph {mode} checker") + self._description = _translate( + "Checker", "Check if the network graph is valid") @timer def _connectivity(self, summary, status, graph): diff --git a/src/Checker/Study.py b/src/Checker/Study.py index 868ac104..b5287e6f 100644 --- a/src/Checker/Study.py +++ b/src/Checker/Study.py @@ -30,7 +30,8 @@ class StudyNetworkReachChecker(AbstractModelChecker): super(StudyNetworkReachChecker, self).__init__() self._name = _translate("Checker", "Study network reach checker") - self._description = _translate("Checker", "Check if exists at least one reach for study") + self._description = _translate( + "Checker", "Check if exists at least one reach for study") def run(self, study): if study is None: @@ -54,12 +55,14 @@ class StudyNetworkReachChecker(AbstractModelChecker): self._status = STATUS.OK return True + class StudyGeometryChecker(AbstractModelChecker): def __init__(self): super(StudyGeometryChecker, self).__init__() self._name = _translate("Checker", "Study geometry checker") - self._description = _translate("Checker", "Check if exists geometry for each reach of study") + self._description = _translate( + "Checker", "Check if exists geometry for each reach of study") self._reachs = [] def run(self, study): @@ -128,6 +131,7 @@ class DummyWARNING(AbstractModelChecker): return True + class DummyERROR(AbstractModelChecker): def __init__(self): super(DummyERROR, self).__init__() diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index d90c6478..f7e09f3d 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -25,12 +25,13 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() + class BoundaryCondition(SQLSubModel): _sub_classes = [] _id_cnt = 0 - def __init__(self, id:int = -1, name:str = "", - status = None): + def __init__(self, id: int = -1, name: str = "", + status=None): super(BoundaryCondition, self).__init__() self._status = status @@ -101,7 +102,7 @@ class BoundaryCondition(SQLSubModel): return res @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): new = [] tab = data["tab"] @@ -115,9 +116,9 @@ class BoundaryCondition(SQLSubModel): t = row[2] ctor = cls._get_ctor_from_type(t) bc = ctor( - id = row[0], - name = row[1], - status = data['status'] + id=row[0], + name=row[1], + status=data['status'] ) bc.node = None @@ -143,7 +144,7 @@ class BoundaryCondition(SQLSubModel): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): tab = data["tab"] execute(f"DELETE FROM boundary_condition WHERE id = {self.id}") @@ -155,7 +156,7 @@ class BoundaryCondition(SQLSubModel): sql = ( "INSERT INTO " + - "boundary_condition(id, name, type, tab, node) "+ + "boundary_condition(id, name, type, tab, node) " + "VALUES (" + f"{self.id}, '{self._sql_format(self._name)}', " + f"'{self._sql_format(self._type)}', '{tab}', {node}" + @@ -170,7 +171,7 @@ class BoundaryCondition(SQLSubModel): sql = ( "INSERT INTO " + - "boundary_condition_data(ind, data0, data1, bc) "+ + "boundary_condition_data(ind, data0, data1, bc) " + f"VALUES ({ind}, '{data0}', {data1}, {self.id})" ) execute(sql) @@ -246,7 +247,7 @@ class BoundaryCondition(SQLSubModel): new_1 = self._default_1 if len(header) != 0: - for i in [0,1]: + for i in [0, 1]: for j in range(len(header)): if self._header[i] == header[j]: if i == 0: @@ -259,13 +260,13 @@ class BoundaryCondition(SQLSubModel): return (new_0, new_1) - def add(self, index:int): + def add(self, index: int): value = (self._default_0, self._default_1) self._data.insert(index, value) self._status.modified() return value - def insert(self, index:int, value): + def insert(self, index: int, value): self._data.insert(index, value) self._status.modified() @@ -306,29 +307,28 @@ class BoundaryCondition(SQLSubModel): l.append(r) return l - def _set_i_c_v(self, index, column, value): v = list(self._data[index]) v[column] = self._types[column](value) self._data[index] = tuple(v) self._status.modified() - def set_i_0(self, index:int, value): + def set_i_0(self, index: int, value): self._set_i_c_v(index, 0, value) - def set_i_1(self, index:int, value): + def set_i_1(self, index: int, value): self._set_i_c_v(index, 1, value) @timer def convert(self, cls): - new = cls(name = self.name, status = self._status) + new = cls(name=self.name, status=self._status) new.node = self.node for i, _ in enumerate(self.data): new.add(i) - for i in [0,1]: - for j in [0,1]: + for i in [0, 1]: + for j in [0, 1]: if self._header[i] == new.header[j]: for ind, v in self.data: try: diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py index eb612d9f..44499316 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionList.py +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -30,6 +30,7 @@ from Model.BoundaryCondition.BoundaryConditionTypes import ( Solid, ) + class BoundaryConditionList(PamhyrModelListWithTab): _tabs_list = ["liquid", "solid", "suspenssion"] _sub_classes = [ @@ -37,8 +38,8 @@ class BoundaryConditionList(PamhyrModelListWithTab): ] @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data['status']) + def _sql_load(cls, execute, data=None): + new = cls(status=data['status']) if data is None: data = {} @@ -51,7 +52,7 @@ class BoundaryConditionList(PamhyrModelListWithTab): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): execute("DELETE FROM boundary_condition") if data is None: @@ -60,7 +61,7 @@ class BoundaryConditionList(PamhyrModelListWithTab): for tab in self._tabs: data["tab"] = tab for bc in self._tabs[tab]: - bc._sql_save(execute, data = data) + bc._sql_save(execute, data=data) return True diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py index af3c91fd..176bb788 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -22,7 +22,7 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition class NotDefined(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(NotDefined, self).__init__(id=id, name=name, status=status) self._type = "ND" @@ -32,9 +32,11 @@ class NotDefined(BoundaryCondition): def _default_0(self): return 0.0 + class PonctualContribution(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): - super(PonctualContribution, self).__init__(id=id, name=name, status=status) + def __init__(self, id: int = -1, name: str = "", status=None): + super(PonctualContribution, self).__init__( + id=id, name=name, status=status) self._type = "PC" self._header = ["time", "discharge"] @@ -44,8 +46,9 @@ class PonctualContribution(BoundaryCondition): def compatibility(cls): return ["liquid"] + class TimeOverZ(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(TimeOverZ, self).__init__(id=id, name=name, status=status) self._type = "TZ" @@ -56,9 +59,11 @@ class TimeOverZ(BoundaryCondition): def compatibility(cls): return ["liquid"] + class TimeOverDischarge(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): - super(TimeOverDischarge, self).__init__(id=id, name=name, status=status) + def __init__(self, id: int = -1, name: str = "", status=None): + super(TimeOverDischarge, self).__init__( + id=id, name=name, status=status) self._type = "TD" self._header = ["time", "discharge"] @@ -68,8 +73,9 @@ class TimeOverDischarge(BoundaryCondition): def compatibility(cls): return ["liquid"] + class ZOverDischarge(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(ZOverDischarge, self).__init__(id=id, name=name, status=status) self._type = "ZD" @@ -86,7 +92,7 @@ class ZOverDischarge(BoundaryCondition): class Solid(BoundaryCondition): - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(Solid, self).__init__(id=id, name=name, status=status) self.d50 = 0.002 diff --git a/src/Model/Except.py b/src/Model/Except.py index 7d5e3959..ce667e38 100644 --- a/src/Model/Except.py +++ b/src/Model/Except.py @@ -45,8 +45,9 @@ def exception_message_box(exception): # Custom error # ################ + class ExeceptionWithMessageBox(Exception): - def __init__(self, title = "Exeception"): + def __init__(self, title="Exeception"): self.title = title def header(self): @@ -72,7 +73,7 @@ class ExeceptionWithMessageBox(Exception): class NotImplementedMethodeError(ExeceptionWithMessageBox): def __init__(self, obj, func): super(NotImplementedMethodeError, self).__init__( - title = _translate("Exception", "Method not implemented") + title=_translate("Exception", "Method not implemented") ) self.obj = obj @@ -104,10 +105,11 @@ class NotImplementedMethodeError(ExeceptionWithMessageBox): f" '{self.obj.__class__}'" ) + class FileFormatError(ExeceptionWithMessageBox): def __init__(self, filename, reason): super(FileFormatError, self).__init__( - title = _translate("Exception", "FileFormatError") + title=_translate("Exception", "FileFormatError") ) self.reason = reason @@ -137,7 +139,7 @@ class FileFormatError(ExeceptionWithMessageBox): class ClipboardFormatError(ExeceptionWithMessageBox): def __init__(self, mime=None, header=None, data=None): super(ClipboardFormatError, self).__init__( - title = _translate("Exception", "Clipboard format error") + title=_translate("Exception", "Clipboard format error") ) self._mime = mime diff --git a/src/Model/Friction/Friction.py b/src/Model/Friction/Friction.py index 7e29e931..baea7397 100644 --- a/src/Model/Friction/Friction.py +++ b/src/Model/Friction/Friction.py @@ -20,8 +20,9 @@ from tools import trace, timer from Model.Tools.PamhyrDB import SQLSubModel + class Friction(SQLSubModel): - def __init__(self, name:str = "", status = None): + def __init__(self, name: str = "", status=None): super(Friction, self).__init__() self._status = status @@ -57,7 +58,7 @@ class Friction(SQLSubModel): return True @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): new = [] reach = data["parent"] # Reach object status = data["status"] @@ -78,7 +79,7 @@ class Friction(SQLSubModel): es = next(filter(lambda s: s.id == row[4], stricklers)) # Create friction - sec = cls(status = status) + sec = cls(status=status) sec.edge = reach sec.begin_kp = row[1] sec.end_kp = row[2] @@ -87,7 +88,7 @@ class Friction(SQLSubModel): yield ind, sec - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): ind = data["ind"] execute( "INSERT INTO " + @@ -119,7 +120,7 @@ class Friction(SQLSubModel): self._edge = edge if (edge is not None and self._begin_kp == 0.0 and - self._end_kp == 0.0): + self._end_kp == 0.0): self._begin_kp = self._edge.reach.get_kp_min() self._end_kp = self._edge.reach.get_kp_max() self._status.modified() diff --git a/src/Model/Friction/FrictionList.py b/src/Model/Friction/FrictionList.py index 777729c7..9531affa 100644 --- a/src/Model/Friction/FrictionList.py +++ b/src/Model/Friction/FrictionList.py @@ -28,6 +28,7 @@ from Model.Friction.Friction import Friction logger = logging.getLogger() + class FrictionList(PamhyrModelList): _sub_classes = [ Friction @@ -46,8 +47,8 @@ class FrictionList(PamhyrModelList): return True @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data['status']) + def _sql_load(cls, execute, data=None): + new = cls(status=data['status']) new._lst = Friction._sql_load( execute, data @@ -55,7 +56,7 @@ class FrictionList(PamhyrModelList): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): reach = data["reach"] execute(f"DELETE FROM friction WHERE reach = {reach.id}") @@ -63,7 +64,7 @@ class FrictionList(PamhyrModelList): ind = 0 for friction in self._lst: data["ind"] = ind - ok &= friction._sql_save(execute, data = data) + ok &= friction._sql_save(execute, data=data) ind += 1 return ok @@ -76,7 +77,7 @@ class FrictionList(PamhyrModelList): lambda p: p[1], sorted( self._lst, - key = lambda p: p[0] + key=lambda p: p[0] ) ) ) @@ -93,7 +94,7 @@ class FrictionList(PamhyrModelList): return self.lst def new(self, index): - n = Friction(status = self._status) + n = Friction(status=self._status) self._lst.insert(index, n) self._status.modified() return n diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index cfea71a4..d6deead8 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -18,8 +18,9 @@ from Model.Except import NotImplementedMethodeError + class Point(object): - def __init__(self, name:str = "", profile=None, status = None): + def __init__(self, name: str = "", profile=None, status=None): super(Point, self).__init__() self._status = status @@ -28,7 +29,6 @@ class Point(object): self._profile = profile self._sl = None - @property def sl(self): if self._sl is None: diff --git a/src/Model/Geometry/PointXY.py b/src/Model/Geometry/PointXY.py index 7448d045..3c958721 100644 --- a/src/Model/Geometry/PointXY.py +++ b/src/Model/Geometry/PointXY.py @@ -20,10 +20,11 @@ from math import dist from Model.Geometry.Point import Point + class PointAC(Point): - def __init__(self, a:float = 0.0, c:float = 0.0, - name: str = "", status = None): - super(PointXY, self).__init__(name = name, status = status) + def __init__(self, a: float = 0.0, c: float = 0.0, + name: str = "", status=None): + super(PointXY, self).__init__(name=name, status=status) self._a = float(a) self._c = float(c) diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 6f1b5784..9a1082a5 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -22,12 +22,14 @@ import numpy as np from Model.Tools.PamhyrDB import SQLSubModel from Model.Geometry.Point import Point + class PointXYZ(Point, SQLSubModel): _sub_classes = [] - def __init__(self, x:float = 0.0, y:float = 0.0, z:float = 0.0, - name:str = "", profile = None, status = None): - super(PointXYZ, self).__init__(name=name, profile=profile, status=status) + def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0, + name: str = "", profile=None, status=None): + super(PointXYZ, self).__init__( + name=name, profile=profile, status=status) self._x = float(x) self._y = float(y) @@ -69,7 +71,7 @@ class PointXYZ(Point, SQLSubModel): return True @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): status = data["status"] profile = data["profile"] @@ -89,10 +91,10 @@ class PointXYZ(Point, SQLSubModel): sl = row[5] new = cls( - name = name, - x = x, y = y, z = z, - profile = profile, - status = status + name=name, + x=x, y=y, z=z, + profile=profile, + status=status ) if sl == -1 or sl == None: @@ -107,7 +109,7 @@ class PointXYZ(Point, SQLSubModel): yield ind, new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): profile = data["profile"] ind = data["ind"] @@ -115,7 +117,7 @@ class PointXYZ(Point, SQLSubModel): sql = ( "INSERT OR REPLACE INTO " + - "geometry_pointXYZ(ind, name, x, y, z, profile, sl) "+ + "geometry_pointXYZ(ind, name, x, y, z, profile, sl) " + "VALUES (" + f"{ind}, '{self._sql_format(self._name)}', " + f"{self.x}, {self.y}, {self.z}, " + @@ -126,7 +128,6 @@ class PointXYZ(Point, SQLSubModel): return True - @classmethod def from_data(cls, header, data): point = None @@ -149,7 +150,6 @@ class PointXYZ(Point, SQLSubModel): return point - def __repr__(self): return f"({self._x}, {self._y}, {self._z}, {self._name})" diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 6290498c..d28da7d6 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -25,14 +25,15 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() + class Profile(object): _id_cnt = 0 - def __init__(self, id:int = -1, num:int = 0, - kp:float = 0.0, name:str = "", - code1:int = 0, code2:int = 0, - _type:str = "", reach = None, - status = None): + def __init__(self, id: int = -1, num: int = 0, + kp: float = 0.0, name: str = "", + code1: int = 0, code2: int = 0, + _type: str = "", reach=None, + status=None): super(Profile, self).__init__() self._status = status @@ -71,7 +72,7 @@ class Profile(object): lambda p: p[1], sorted( self._points, - key = lambda p: p[0] + key=lambda p: p[0] ) ) ) @@ -185,7 +186,7 @@ class Profile(object): self._profile_type = value self._status.modified() - def point(self, i:int): + def point(self, i: int): if i < len(self.points): return self.points[i] @@ -200,8 +201,7 @@ class Profile(object): return [point for point in self.points if point.point_is_named()] - - def insert_point(self, index: int, point:Point): + def insert_point(self, index: int, point: Point): """Insert point at index. Args: @@ -214,7 +214,6 @@ class Profile(object): self.points.insert(index, point) self._status.modified() - def delete(self, indexes: int): """Delete points at index @@ -281,11 +280,11 @@ class Profile(object): @timer def sort(self, column, is_reversed: bool = False): - predicate = lambda p: p.x + def predicate(p): return p.x if column == 'y': - predicate = lambda p: p.y + def predicate(p): return p.y elif column == 'z': - predicate = lambda p: p.z + def predicate(p): return p.z self.points = sorted( self.points, diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 929928b4..de0fcd8c 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -30,6 +30,7 @@ from Model.Geometry.Vector_1d import Vector1d logger = logging.getLogger() + class ProfileXYZ(Profile, SQLSubModel): _sub_classes = [ PointXYZ, @@ -39,11 +40,11 @@ class ProfileXYZ(Profile, SQLSubModel): id: int = -1, name: str = "", kp: float = 0., - reach = None, - num = 0, + reach=None, + num=0, nb_point: int = 0, code1: int = 0, code2: int = 0, - status = None): + status=None): """ProfileXYZ constructor Args: @@ -57,14 +58,14 @@ class ProfileXYZ(Profile, SQLSubModel): Nothing. """ super(ProfileXYZ, self).__init__( - id = id, - num = num, - name = name, - kp = kp, - code1 = code1, code2 = code2, - _type = "XYZ", - reach = reach, - status = status, + id=id, + num=num, + name=name, + kp=kp, + code1=code1, code2=code2, + _type="XYZ", + reach=reach, + status=status, ) @classmethod @@ -103,7 +104,7 @@ class ProfileXYZ(Profile, SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): profiles = [] status = data["status"] reach = data["reach"] @@ -128,11 +129,11 @@ class ProfileXYZ(Profile, SQLSubModel): sl = row[7] new = cls( - id=id, num = num, - name = name, kp = kp, - code1 = code1, code2 = code2, - reach = data["parent"], - status = status + id=id, num=num, + name=name, kp=kp, + code1=code1, code2=code2, + reach=data["parent"], + status=status ) if sl == -1 or sl == None: @@ -154,7 +155,7 @@ class ProfileXYZ(Profile, SQLSubModel): # return profiles - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): ok = True ind = data["ind"] @@ -162,7 +163,7 @@ class ProfileXYZ(Profile, SQLSubModel): sql = ( "INSERT OR REPLACE INTO " + - "geometry_profileXYZ(id, ind, name, reach, kp, num, code1, code2, sl) "+ + "geometry_profileXYZ(id, ind, name, reach, kp, num, code1, code2, sl) " + "VALUES (" + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + f"{self.reach.id}, {self.kp}, {self.num}, " + @@ -225,7 +226,6 @@ class ProfileXYZ(Profile, SQLSubModel): return point - def x(self): return [point.x for point in self.points] @@ -374,9 +374,9 @@ class ProfileXYZ(Profile, SQLSubModel): constant = 0.0 if (first_named_point is not None and - last_named_point is not None): + last_named_point is not None): if (first_named_point != last_named_point and - first_named_point.x != last_named_point.x): + first_named_point.x != last_named_point.x): vector = Vector1d(first_named_point, last_named_point) normalized_direction_vec = vector.normalized_direction_vector() else: diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index a6ed514c..c2df3a75 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -36,6 +36,7 @@ from Model.Except import FileFormatError, exception_message_box logger = logging.getLogger() + class Reach(SQLSubModel): _sub_classes = [ ProfileXYZ, @@ -59,17 +60,17 @@ class Reach(SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data["status"], parent = data["parent"]) + def _sql_load(cls, execute, data=None): + new = cls(status=data["status"], parent=data["parent"]) new._profiles = ProfileXYZ._sql_load( execute, - data = data + data=data ) return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): # Delete old data execute(f"DELETE FROM geometry_profileXYZ WHERE reach = {self.id}") # execute(f"DELETE FROM geometry_pointXYZ") @@ -115,7 +116,7 @@ class Reach(SQLSubModel): lambda p: p[1], sorted( self._profiles, - key = lambda p: p[0] + key=lambda p: p[0] ) ) ) @@ -195,7 +196,6 @@ class Reach(SQLSubModel): self._update_profile_numbers() self._status.modified() - def delete(self, indexes): """Delete some elements in profile list @@ -242,7 +242,6 @@ class Reach(SQLSubModel): self._update_profile_numbers() self._status.modified() - def move_up_profile(self, index: int): if index < len(self.profiles): next = index - 1 @@ -298,7 +297,6 @@ class Reach(SQLSubModel): def get_kp_max(self): return max(self.get_kp()) - # Sediment Layers def get_sl(self): @@ -437,7 +435,6 @@ class Reach(SQLSubModel): def get_guidelines_z(self): return self._map_guidelines_points(lambda p: p.z) - # Sort @timer @@ -482,7 +479,8 @@ class Reach(SQLSubModel): try: list_profile, list_header = self.read_file_st(str(file_path_name)) - profile_header = ["num", "code1", "code2", "nb_point", "kp", "name"] + profile_header = ["num", "code1", + "code2", "nb_point", "kp", "name"] if list_profile and list_header: for ind, profile in enumerate(list_profile): diff --git a/src/Model/Geometry/Vector_1d.py b/src/Model/Geometry/Vector_1d.py index a3da9358..e136f121 100644 --- a/src/Model/Geometry/Vector_1d.py +++ b/src/Model/Geometry/Vector_1d.py @@ -19,6 +19,7 @@ import numpy as np from Model.Geometry.PointXYZ import PointXYZ + class Vector1d: def __init__(self, a: PointXYZ, b: PointXYZ): self.A = a diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py index a40e0261..20bbb3d0 100644 --- a/src/Model/InitialConditions/InitialConditions.py +++ b/src/Model/InitialConditions/InitialConditions.py @@ -22,12 +22,13 @@ from functools import reduce from Model.Tools.PamhyrDB import SQLSubModel + class Data(SQLSubModel): - def __init__(self, name:str = "", - comment:str = "", reach = None, - kp:float = 0.0, discharge:float = 0.0, - height:float = 0.0, - status = None): + def __init__(self, name: str = "", + comment: str = "", reach=None, + kp: float = 0.0, discharge: float = 0.0, + height: float = 0.0, + status=None): super(Data, self).__init__() self._status = status @@ -73,7 +74,7 @@ class Data(SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): id = data["reach"].id table = execute( "SELECT ind, name, comment, kp, discharge, height " + @@ -95,20 +96,20 @@ class Data(SQLSubModel): height = row[5] d = cls( - reach = data["reach"], - status = data["status"], - name = name, - comment = comment, - kp = kp, - discharge = discharge, - height = height, + reach=data["reach"], + status=data["status"], + name=name, + comment=comment, + kp=kp, + discharge=discharge, + height=height, ) new[ind] = d return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): ind = data["ind"] execute( @@ -124,16 +125,15 @@ class Data(SQLSubModel): return True - def copy(self): new = Data( - name = self.name, - comment = self._comment, - kp = self._kp, - discharge = self._discharge, - height = self._height, - reach = self._reach, - status = self._status, + name=self.name, + comment=self._comment, + kp=self._kp, + discharge=self._discharge, + height=self._height, + reach=self._reach, + status=self._status, ) return new @@ -216,7 +216,7 @@ class InitialConditions(SQLSubModel): Data ] - def __init__(self, reach = None, status = None): + def __init__(self, reach=None, status=None): super(InitialConditions, self).__init__() self._status = status @@ -233,21 +233,21 @@ class InitialConditions(SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): new = cls( - reach = data["reach"], - status = data["status"] + reach=data["reach"], + status=data["status"] ) new._data = Data._sql_load( execute, - data = data + data=data ) if new._data is not None: yield new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): ok = True ind = 0 @@ -286,7 +286,7 @@ class InitialConditions(SQLSubModel): self._status.modified() def new(self, index): - n = Data(reach = self._reach, status = self._status) + n = Data(reach=self._reach, status=self._status) self._data.insert(index, n) self._status.modified() @@ -319,7 +319,6 @@ class InitialConditions(SQLSubModel): self._data.sort(reverse=reverse, key=key) self._status.modified() - def _data_get(self, key): return list( map( @@ -339,8 +338,8 @@ class InitialConditions(SQLSubModel): def _sort_by_z_and_kp(self, profiles): profiles.sort( - reverse = False, - key = lambda p: p.kp + reverse=False, + key=lambda p: p.kp ) first_z = profiles[0].z() @@ -348,11 +347,11 @@ class InitialConditions(SQLSubModel): if first_z > last_z: profiles.sort( - reverse = True, - key = lambda p: p.kp + reverse=True, + key=lambda p: p.kp ) - def generate_growing_constante_height(self, height:float): + def generate_growing_constante_height(self, height: float): self._data = [] profiles = self._reach.reach.profiles @@ -360,7 +359,7 @@ class InitialConditions(SQLSubModel): prev = None for profile in profiles: - new = Data(reach = self._reach, status = self._status) + new = Data(reach=self._reach, status=self._status) new["kp"] = profile.kp if prev is None: @@ -379,11 +378,11 @@ class InitialConditions(SQLSubModel): is_reverse = True self._data.sort( - reverse = not is_reverse, - key = lambda d: d['kp'] + reverse=not is_reverse, + key=lambda d: d['kp'] ) - def generate_discharge(self, discharge:float): + def generate_discharge(self, discharge: float): self._new = [] for d in self._data: diff --git a/src/Model/InitialConditions/InitialConditionsDict.py b/src/Model/InitialConditions/InitialConditionsDict.py index f2bd038f..5fc8b650 100644 --- a/src/Model/InitialConditions/InitialConditionsDict.py +++ b/src/Model/InitialConditions/InitialConditionsDict.py @@ -23,21 +23,22 @@ from tools import trace, timer from Model.Tools.PamhyrDict import PamhyrModelDict from Model.InitialConditions.InitialConditions import InitialConditions + class InitialConditionsDict(PamhyrModelDict): _sub_classes = [ InitialConditions, ] @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data["status"]) + def _sql_load(cls, execute, data=None): + new = cls(status=data["status"]) for reach in data["edges"]: data["reach"] = reach ic = InitialConditions._sql_load( execute, - data = data + data=data ) if ic is not None: @@ -45,7 +46,7 @@ class InitialConditionsDict(PamhyrModelDict): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): ok = True if data is None: data = {} @@ -58,7 +59,6 @@ class InitialConditionsDict(PamhyrModelDict): return ok - def get(self, key): if key in self._dict: v = self._dict[key] @@ -72,6 +72,6 @@ class InitialConditionsDict(PamhyrModelDict): return new def new(self, reach): - new = InitialConditions(reach = reach, status = self._status) + new = InitialConditions(reach=reach, status=self._status) self.set(reach, new) return new diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index 727cf21b..de117744 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -25,11 +25,12 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() + class LateralContribution(SQLSubModel): _sub_classes = [] _id_cnt = 0 - def __init__(self, id:int = -1, name:str = "", status = None): + def __init__(self, id: int = -1, name: str = "", status=None): super(LateralContribution, self).__init__() self._status = status @@ -39,7 +40,6 @@ class LateralContribution(SQLSubModel): else: self.id = id - self._name = name self._type = "" self._edge = None @@ -49,7 +49,8 @@ class LateralContribution(SQLSubModel): self._header = [] self._types = [float, float] - LateralContribution._id_cnt = max(LateralContribution._id_cnt + 1, self.id) + LateralContribution._id_cnt = max( + LateralContribution._id_cnt + 1, self.id) @classmethod def _sql_create(cls, execute): @@ -99,7 +100,7 @@ class LateralContribution(SQLSubModel): return res @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): new = [] tab = data["tab"] @@ -112,9 +113,9 @@ class LateralContribution(SQLSubModel): t = row[2] ctor = cls._get_ctor_from_type(t) lc = ctor( - id = row[0], - name = row[1], - status = data['status'] + id=row[0], + name=row[1], + status=data['status'] ) lc.edge = None if row[3] != -1: @@ -141,7 +142,7 @@ class LateralContribution(SQLSubModel): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): tab = data["tab"] execute(f"DELETE FROM lateral_contribution WHERE id = {self.id}") @@ -153,7 +154,7 @@ class LateralContribution(SQLSubModel): sql = ( "INSERT INTO " + - "lateral_contribution(id, name, type, tab, edge, begin_kp, end_kp) "+ + "lateral_contribution(id, name, type, tab, edge, begin_kp, end_kp) " + "VALUES (" + f"{self.id}, '{self._sql_format(self._name)}', " + f"'{self._sql_format(self._type)}', '{tab}', {edge}, " + @@ -169,7 +170,7 @@ class LateralContribution(SQLSubModel): sql = ( "INSERT INTO " + - "lateral_contribution_data(ind, data0, data1, lc) "+ + "lateral_contribution_data(ind, data0, data1, lc) " + f"VALUES ({ind}, '{data0}', {data1}, {self.id})" ) execute(sql) @@ -282,7 +283,7 @@ class LateralContribution(SQLSubModel): new_1 = self._default_1 if len(header) != 0: - for i in [0,1]: + for i in [0, 1]: for j in range(len(header)): if self._header[i] == header[j]: if i == 0: @@ -295,13 +296,13 @@ class LateralContribution(SQLSubModel): return (new_0, new_1) - def add(self, index:int): + def add(self, index: int): value = (self._default_0, self._default_1) self._data.insert(index, value) self._status.modified() return value - def insert(self, index:int, value): + def insert(self, index: int, value): self._data.insert(index, value) self._status.modified() @@ -342,22 +343,21 @@ class LateralContribution(SQLSubModel): l.append(r) return l - def _set_i_c_v(self, index, column, value): v = list(self._data[index]) v[column] = self._types[column](value) self._data[index] = tuple(v) self._status.modified() - def set_i_0(self, index:int, value): + def set_i_0(self, index: int, value): self._set_i_c_v(index, 0, value) - def set_i_1(self, index:int, value): + def set_i_1(self, index: int, value): self._set_i_c_v(index, 1, value) @timer def convert(self, cls): - new = cls(name = self.name, status = self._status) + new = cls(name=self.name, status=self._status) new.edge = self.edge new.begin_kp = self.begin_kp new.end_kp = self.end_kp @@ -365,8 +365,8 @@ class LateralContribution(SQLSubModel): for i, _ in enumerate(self.data): new.add(i) - for i in [0,1]: - for j in [0,1]: + for i in [0, 1]: + for j in [0, 1]: if self._header[i] == new.header[j]: for ind, v in self.data: try: diff --git a/src/Model/LateralContribution/LateralContributionList.py b/src/Model/LateralContribution/LateralContributionList.py index 8d0ba37d..a69330ae 100644 --- a/src/Model/LateralContribution/LateralContributionList.py +++ b/src/Model/LateralContribution/LateralContributionList.py @@ -27,6 +27,7 @@ from Model.LateralContribution.LateralContributionTypes import ( NotDefined, LateralContrib, Rain, Evaporation, ) + class LateralContributionList(PamhyrModelListWithTab): _tabs_list = ["liquid", "solid", "suspenssion"] _sub_classes = [ @@ -34,8 +35,8 @@ class LateralContributionList(PamhyrModelListWithTab): ] @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data['status']) + def _sql_load(cls, execute, data=None): + new = cls(status=data['status']) if data is None: data = {} @@ -48,7 +49,7 @@ class LateralContributionList(PamhyrModelListWithTab): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): execute("DELETE FROM lateral_contribution") if data is None: @@ -57,7 +58,7 @@ class LateralContributionList(PamhyrModelListWithTab): for tab in self._tabs: data["tab"] = tab for lc in self._tabs[tab]: - lc._sql_save(execute, data = data) + lc._sql_save(execute, data=data) return True diff --git a/src/Model/LateralContribution/LateralContributionTypes.py b/src/Model/LateralContribution/LateralContributionTypes.py index d669266a..4f0a843c 100644 --- a/src/Model/LateralContribution/LateralContributionTypes.py +++ b/src/Model/LateralContribution/LateralContributionTypes.py @@ -22,7 +22,7 @@ from Model.LateralContribution.LateralContribution import LateralContribution class NotDefined(LateralContribution): - def __init__(self, id:int = -1, name:str = "", status=None): + def __init__(self, id: int = -1, name: str = "", status=None): super(NotDefined, self).__init__(id=id, name=name, status=status) self._type = "ND" @@ -32,8 +32,9 @@ class NotDefined(LateralContribution): def _default_0(self): return 0.0 + class LateralContrib(LateralContribution): - def __init__(self, id:int = -1, name:str = "", status=None): + def __init__(self, id: int = -1, name: str = "", status=None): super(LateralContrib, self).__init__(id=id, name=name, status=status) self._type = "LC" @@ -44,8 +45,9 @@ class LateralContrib(LateralContribution): def compatibility(cls): return ["liquid"] + class Rain(LateralContribution): - def __init__(self, id:int = -1, name:str = "", status=None): + def __init__(self, id: int = -1, name: str = "", status=None): super(Rain, self).__init__(id=id, name=name, status=status) self._type = "RA" @@ -56,8 +58,9 @@ class Rain(LateralContribution): def compatibility(cls): return ["liquid"] + class Evaporation(LateralContribution): - def __init__(self, id:int = -1, name:str = "", status=None): + def __init__(self, id: int = -1, name: str = "", status=None): super(Evaporation, self).__init__(id=id, name=name, status=status) self._type = "EV" diff --git a/src/Model/Network/Edge.py b/src/Model/Network/Edge.py index 12249943..dddd8418 100644 --- a/src/Model/Network/Edge.py +++ b/src/Model/Network/Edge.py @@ -18,13 +18,14 @@ from Model.Network.Node import Node + class Edge(object): _id_cnt = 0 - def __init__(self, id:int, name:str, - node1:Node = None, - node2:Node = None, - status = None): + def __init__(self, id: int, name: str, + node1: Node = None, + node2: Node = None, + status=None): super(Edge, self).__init__() self._status = status diff --git a/src/Model/Network/Graph.py b/src/Model/Network/Graph.py index 62948a2b..d617ba77 100644 --- a/src/Model/Network/Graph.py +++ b/src/Model/Network/Graph.py @@ -21,8 +21,9 @@ from functools import reduce from Model.Network.Node import Node from Model.Network.Edge import Edge + class Graph(object): - def __init__(self, status = None): + def __init__(self, status=None): super(Graph, self).__init__() self._status = status @@ -83,7 +84,7 @@ class Graph(object): False ) - def node(self, node_name:str): + def node(self, node_name: str): node = list( filter( lambda n: n.name == node_name, @@ -96,7 +97,7 @@ class Graph(object): return node[0] - def edge(self, edge_name:str): + def edge(self, edge_name: str): edge = list( filter( lambda e: e.name == edge_name, @@ -109,12 +110,12 @@ class Graph(object): return edge[0] - def _create_node(self, x:float, y:float): + def _create_node(self, x: float, y: float): node = self._node_ctor( -1, "", - x = x, y = y, - status = self._status + x=x, y=y, + status=self._status ) return node @@ -123,14 +124,14 @@ class Graph(object): self._status.modified() return node - def add_node(self, x:float = 0.0, y:float = 0.0): + def add_node(self, x: float = 0.0, y: float = 0.0): node = self._create_node(x, y) return self._add_node(node) def insert_node(self, node): return self._add_node(node) - def remove_node(self, node_name:str): + def remove_node(self, node_name: str): self._nodes = list( filter( lambda n: n.name != node_name, @@ -139,19 +140,18 @@ class Graph(object): ) self._status.modified() - def create_node(self, x:float = 0.0, y:float = 0.0): + def create_node(self, x: float = 0.0, y: float = 0.0): node = self._create_node(x, y) return node - def _create_edge(self, n1:Node, n2:Node): + def _create_edge(self, n1: Node, n2: Node): edge = self._edge_ctor( -1, "", n1, n2, - status = self._status + status=self._status ) return edge - def _add_edge(self, edge): # This edge already exists ? if any(filter(lambda e: (e.node1 == edge.node1 and @@ -164,17 +164,17 @@ class Graph(object): self._status.modified() return edge - def add_edge(self, n1:Node, n2:Node): + def add_edge(self, n1: Node, n2: Node): edge = self._create_edge(n1, n2) return self._add_edge(edge) def insert_edge(self, edge): return self._add_edge(edge) - def create_edge(self, n1:Node, n2:Node): + def create_edge(self, n1: Node, n2: Node): return self._create_edge(n1, n2) - def remove_edge(self, edge_name:str): + def remove_edge(self, edge_name: str): self._edges = list( filter( lambda e: e.name != edge_name, diff --git a/src/Model/Network/Node.py b/src/Model/Network/Node.py index 7b8f52ba..d79b71cf 100644 --- a/src/Model/Network/Node.py +++ b/src/Model/Network/Node.py @@ -18,12 +18,13 @@ from Model.Network.Point import Point + class Node(object): _id_cnt = 0 - def __init__(self, id:int, name:str, - x:float = 0.0, y:float = 0.0, - status = None): + def __init__(self, id: int, name: str, + x: float = 0.0, y: float = 0.0, + status=None): super(Node, self).__init__() self._status = status @@ -73,7 +74,6 @@ class Node(object): def y(self): return self.pos.y - def setPos(self, x, y): self.pos.x = x self.pos.y = y diff --git a/src/Model/Network/Point.py b/src/Model/Network/Point.py index 0bb12609..38f0c83f 100644 --- a/src/Model/Network/Point.py +++ b/src/Model/Network/Point.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- class Point(object): - def __init__(self, x:float, y:float): + def __init__(self, x: float, y: float): super(Point, self).__init__() self.x = x diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 3258b0e1..7c9ed54c 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -24,8 +24,9 @@ from Model.Results.River.River import River logger = logging.getLogger() + class Results(object): - def __init__(self, study = None): + def __init__(self, study=None): self._study = study self._river = River(self._study) diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 80cd8450..7597d751 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -20,11 +20,12 @@ from datetime import datetime logger = logging.getLogger() + class Profile(object): def __init__(self, profile, study): self._study = study - self._profile = profile # Source profile in the study - self._data = {} # Dict of dict {: {: , ...}, ...} + self._profile = profile # Source profile in the study + self._data = {} # Dict of dict {: {: , ...}, ...} def __len__(self): return len(self._data) @@ -58,10 +59,11 @@ class Profile(object): def get_ts_key(self, timestamp, key): return self._data[timestamp][key] + class Reach(object): def __init__(self, reach, study): self._study = study - self._reach = reach # Source reach in the study + self._reach = reach # Source reach in the study self._profiles = list( map( lambda p: Profile(p, self._study), @@ -90,6 +92,7 @@ class Reach(object): def set(self, profile_id, timestamp, key, data): self._profiles[profile_id].set(timestamp, key, data) + class River(object): def __init__(self, study): self._study = study diff --git a/src/Model/River.py b/src/Model/River.py index d5dccb9e..798a24e1 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -37,16 +37,17 @@ from Model.SedimentLayer.SedimentLayerList import SedimentLayerList from Solver.Solvers import solver_type_list + class RiverNode(Node, SQLSubModel): _sub_classes = [] - def __init__(self, id:str, name:str, - x:float, y:float, - status = None): + def __init__(self, id: str, name: str, + x: float, y: float, + status=None): super(RiverNode, self).__init__( id, name, x, y, - status = status + status=status ) self._locker = None @@ -70,7 +71,7 @@ class RiverNode(Node, SQLSubModel): return True @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): nodes = [] table = execute("SELECT id, name, x, y FROM river_node") @@ -82,11 +83,11 @@ class RiverNode(Node, SQLSubModel): return nodes - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): sql = ( "INSERT OR REPLACE INTO river_node(id, name, x, y) VALUES (" + f"{self.id}, '{self._sql_format(self.name)}', " + - f"{self.x}, {self.y}"+ + f"{self.x}, {self.y}" + ")" ) execute(sql) @@ -108,18 +109,18 @@ class RiverReach(Edge, SQLSubModel): FrictionList, ] - def __init__(self, id:str, name:str, - node1:RiverNode = None, - node2:RiverNode = None, - status = None): + def __init__(self, id: str, name: str, + node1: RiverNode = None, + node2: RiverNode = None, + status=None): super(RiverReach, self).__init__( id, name, node1, node2, - status = status + status=status ) self._reach = Reach(status=self._status, parent=self) - self._frictions = FrictionList(status = self._status) + self._frictions = FrictionList(status=self._status) @classmethod def _sql_create(cls, execute): @@ -143,13 +144,14 @@ class RiverReach(Edge, SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): reachs = [] if data is None: data = {} - table = execute("SELECT id, name, enable, node1, node2 FROM river_reach") + table = execute( + "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]) @@ -161,8 +163,8 @@ class RiverReach(Edge, SQLSubModel): node1 = next(filter(lambda n: n.id == row[3], data["nodes"])) node2 = next(filter(lambda n: n.id == row[4], data["nodes"])) - new = cls(id, name, node1, node2, status = data["status"]) - new.enable(enable = enable) + new = cls(id, name, node1, node2, status=data["status"]) + new.enable(enable=enable) data["reach"] = id data["parent"] = new @@ -174,14 +176,14 @@ class RiverReach(Edge, SQLSubModel): return reachs - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): sql = ( "INSERT OR REPLACE INTO " + - "river_reach(id, name, enable, node1, node2) "+ + "river_reach(id, name, enable, node1, node2) " + "VALUES (" + f"{self.id}, '{self._sql_format(self._name)}', " + f"{self._sql_format(self.is_enable())}," - f"{self.node1.id}, {self.node2.id}"+ + f"{self.node1.id}, {self.node2.id}" + ")" ) execute(sql) @@ -202,6 +204,7 @@ class RiverReach(Edge, SQLSubModel): def frictions(self): return self._frictions + class River(Graph, SQLSubModel): _sub_classes = [ RiverNode, @@ -223,7 +226,8 @@ class River(Graph, SQLSubModel): self._current_reach = None self._boundary_condition = BoundaryConditionList(status=self._status) - self._lateral_contribution = LateralContributionList(status=self._status) + self._lateral_contribution = LateralContributionList( + status=self._status) self._initial_conditions = InitialConditionsDict(status=self._status) self._stricklers = StricklersList(status=self._status) self._parameters = {} @@ -240,7 +244,7 @@ class River(Graph, SQLSubModel): return True @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): new = cls(data["status"]) # Stricklers (Stricklers is load in first because it's needed @@ -251,7 +255,6 @@ class River(Graph, SQLSubModel): ) data["stricklers"] = new._stricklers - # Initial conditions new._sediment_layers = SedimentLayerList._sql_load( execute, @@ -297,7 +300,7 @@ class River(Graph, SQLSubModel): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): objs = (self._nodes + self._edges) objs.append(self._boundary_condition) objs.append(self._initial_conditions) @@ -344,7 +347,6 @@ class River(Graph, SQLSubModel): return ret[0] - @property def parameters(self): return self._parameters @@ -354,8 +356,8 @@ class River(Graph, SQLSubModel): return self._parameters[solver] new = SolverParametersList( - solver_type = solver_type_list[solver], - status = self._status + solver_type=solver_type_list[solver], + status=self._status ) self._parameters[solver] = new self._status.modified() diff --git a/src/Model/Saved.py b/src/Model/Saved.py index 60013cde..52bd0780 100644 --- a/src/Model/Saved.py +++ b/src/Model/Saved.py @@ -20,6 +20,7 @@ import logging logger = logging.getLogger() + class SavedStatus(object): def __init__(self): super(SavedStatus, self).__init__() diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 1438dfa7..69e140bf 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -5,16 +5,17 @@ from tools import trace, timer from Model.Tools.PamhyrDB import SQLSubModel from Model.Except import NotImplementedMethodeError + class Layer(SQLSubModel): _sub_classes = [] _id_cnt = 0 def __init__(self, - id:int = -1, name:str = "", - type = "", - height = 0.0, d50 = 0.0, sigma = 0.0, - critical_constraint = 0.0, - sl = None, status = None): + id: int = -1, name: str = "", + type="", + height=0.0, d50=0.0, sigma=0.0, + critical_constraint=0.0, + sl=None, status=None): super(Layer, self).__init__() self._status = status @@ -111,7 +112,7 @@ class Layer(SQLSubModel): return True @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): new = [] sl = data["sl"] @@ -128,24 +129,24 @@ class Layer(SQLSubModel): ind = row[1] layer = cls( - id = row[0], name = row[2], - type = row[3], height = row[4], - d50 = row[5], sigma = row[6], - critical_constraint = row[7], - sl = sl, status = data['status'] + id=row[0], name=row[2], + type=row[3], height=row[4], + d50=row[5], sigma=row[6], + critical_constraint=row[7], + sl=sl, status=data['status'] ) new[ind] = layer return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): ind = data["ind"] sl = data["sl"] sql = ( "INSERT INTO " + - "sedimentary_layer_layer(id, ind, name, type, height, d50, sigma, critical_constraint, sl) "+ + "sedimentary_layer_layer(id, ind, name, type, height, d50, sigma, critical_constraint, sl) " + "VALUES (" + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + f"'{self._sql_format(self._type)}', {self._height}, " + @@ -162,9 +163,9 @@ class SedimentLayer(SQLSubModel): _sub_classes = [Layer] _id_cnt = 0 - def __init__(self, id:int = -1, - name:str = "", comment:str = "", - status = None): + def __init__(self, id: int = -1, + name: str = "", comment: str = "", + status=None): super(SedimentLayer, self).__init__() self._status = status @@ -242,7 +243,7 @@ class SedimentLayer(SQLSubModel): return True @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): new = [] table = execute( @@ -252,10 +253,10 @@ class SedimentLayer(SQLSubModel): for row in table: sl = cls( - id = row[0], - name = row[1], - comment = row[2], - status = data['status'] + id=row[0], + name=row[1], + comment=row[2], + status=data['status'] ) data["sl"] = sl.id @@ -265,7 +266,7 @@ class SedimentLayer(SQLSubModel): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): if data is None: data = {} @@ -298,7 +299,7 @@ class SedimentLayer(SQLSubModel): self._status.modified() def new(self, index): - n = Layer(sl=self, status = self._status) + n = Layer(sl=self, status=self._status) self.insert(index, n) self._status.modified() return n diff --git a/src/Model/SedimentLayer/SedimentLayerList.py b/src/Model/SedimentLayer/SedimentLayerList.py index 53a1a8fd..d7be0a1e 100644 --- a/src/Model/SedimentLayer/SedimentLayerList.py +++ b/src/Model/SedimentLayer/SedimentLayerList.py @@ -6,18 +6,19 @@ from Model.Except import NotImplementedMethodeError from Model.Tools.PamhyrList import PamhyrModelList from Model.SedimentLayer.SedimentLayer import SedimentLayer + class SedimentLayerList(PamhyrModelList): _sub_classes = [SedimentLayer] @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data["status"]) + def _sql_load(cls, execute, data=None): + new = cls(status=data["status"]) new._lst = SedimentLayer._sql_load(execute, data) return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): ok = True # Delete previous data @@ -34,7 +35,7 @@ class SedimentLayerList(PamhyrModelList): return self.lst def new(self, index): - n = SedimentLayer(status = self._status) + n = SedimentLayer(status=self._status) self.insert(index, n) self._status.modified() return n diff --git a/src/Model/Serializable.py b/src/Model/Serializable.py index 0c083448..fb4ea11d 100644 --- a/src/Model/Serializable.py +++ b/src/Model/Serializable.py @@ -18,6 +18,7 @@ import pickle + class Serializable(): def __init__(self): return diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index ce7ade98..41906ef0 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -22,10 +22,11 @@ from Solver.Solvers import solver_type_list from Model.Tools.PamhyrList import PamhyrModelList + class Parameter(): def __init__(self, - name = "", value = "", - status = None): + name="", value="", + status=None): self._status = status self._name = name @@ -57,14 +58,15 @@ class Parameter(): @classmethod def from_tuple(cls, data, status): - new = cls(status = status) + new = cls(status=status) new["name"] = data[0] new["value"] = data[1] return new + class SolverParametersList(PamhyrModelList): - def __init__(self, solver_type = None, status = None): + def __init__(self, solver_type=None, status=None): super(SolverParametersList, self).__init__() self._status = status @@ -76,7 +78,6 @@ class SolverParametersList(PamhyrModelList): ) ) - @classmethod def _sql_create(cls, execute): execute(""" @@ -97,35 +98,38 @@ class SolverParametersList(PamhyrModelList): if major == minor == "0": if int(release) < 3: - execute(f"UPDATE solver_parameter SET name='mage_implicitation' WHERE name='mage_implication'") - execute(f"UPDATE solver_parameter SET name='mage_iteration_type' WHERE name='mage_iter_type'") + execute( + f"UPDATE solver_parameter SET name='mage_implicitation' WHERE name='mage_implication'") + execute( + f"UPDATE solver_parameter SET name='mage_iteration_type' WHERE name='mage_iter_type'") if int(release) < 4: - solvers = execute("SELECT DISTINCT solver FROM solver_parameter") + solvers = execute( + "SELECT DISTINCT solver FROM solver_parameter") for solver in solvers: if solver != "mage8": continue data = execute("SELECT ind, name, value " + - "FROM solver_parameter "+ - f"WHERE solver = '{solver}' " + - "ORDER BY ind ACS") + "FROM solver_parameter " + + f"WHERE solver = '{solver}' " + + "ORDER BY ind ACS") execute( "DELETE FROM solver_parameter " + f"WHERE solver = '{solver}'" ) - insert = lambda name, value, ind: execute( - "INSERT INTO " + - "solver_parameter(ind, name, value, solver) "+ - "VALUES (" + - f"{ind}, " + - f"'{cls._sql_format(name)}', " + - f"'{cls._sql_format(value)}', " + - f"'{cls._sql_format(solver)}'" + - ")" + def insert(name, value, ind): return execute( + "INSERT INTO " + + "solver_parameter(ind, name, value, solver) " + + "VALUES (" + + f"{ind}, " + + f"'{cls._sql_format(name)}', " + + f"'{cls._sql_format(value)}', " + + f"'{cls._sql_format(solver)}'" + + ")" ) ind = 0 @@ -156,7 +160,7 @@ class SolverParametersList(PamhyrModelList): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): status = data["status"] solvers = execute("SELECT DISTINCT solver FROM solver_parameter") new = {} @@ -167,7 +171,7 @@ class SolverParametersList(PamhyrModelList): continue st = solver_type_list[solver] - n = cls(solver_type = st, status = status) + n = cls(solver_type=st, status=status) table = execute( "SELECT ind, name, value " + @@ -186,7 +190,7 @@ class SolverParametersList(PamhyrModelList): return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): t = self._solver._type execute( "DELETE FROM solver_parameter " + @@ -197,7 +201,7 @@ class SolverParametersList(PamhyrModelList): for param in self._lst: sql = ( "INSERT INTO " + - "solver_parameter(ind, name, value, solver) "+ + "solver_parameter(ind, name, value, solver) " + "VALUES (" + f"{ind}, " + f"'{self._sql_format(param.name)}', " + @@ -241,15 +245,15 @@ class SolverParametersList(PamhyrModelList): self._lst.append( Parameter( - name = key, - value = value, - status = self._status + name=key, + value=value, + status=self._status ) ) self._status.modified() def new(self, index): - n = Parameter(status = self._status) + n = Parameter(status=self._status) self._lst.insert(index, n) self._status.modified() return n diff --git a/src/Model/Stricklers/Stricklers.py b/src/Model/Stricklers/Stricklers.py index e18fd01d..f1c35119 100644 --- a/src/Model/Stricklers/Stricklers.py +++ b/src/Model/Stricklers/Stricklers.py @@ -20,16 +20,17 @@ from tools import trace, timer from Model.Tools.PamhyrDB import SQLSubModel + class Stricklers(SQLSubModel): _id_cnt = 0 _sub_classes = [] - def __init__(self, id:int = -1, - name:str = "", - comment:str = "", - minor:float = 35.0, - medium:float = 15.0, - status = None): + def __init__(self, id: int = -1, + name: str = "", + comment: str = "", + minor: float = 35.0, + medium: float = 15.0, + status=None): super(Stricklers, self).__init__() self._status = status @@ -46,7 +47,6 @@ class Stricklers(SQLSubModel): self._minor = minor self._medium = medium - @classmethod def _sql_create(cls, execute): execute(""" @@ -66,7 +66,7 @@ class Stricklers(SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): stricklers = [] status = data["status"] @@ -86,21 +86,21 @@ class Stricklers(SQLSubModel): medium = row[4] new = cls( - id = id, - name = name, - comment = comment, - minor = minor, medium = medium, - status = status + id=id, + name=name, + comment=comment, + minor=minor, medium=medium, + status=status ) stricklers.append(new) return stricklers - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): sql = ( "INSERT INTO " + - "stricklers(id, name, comment, minor, medium) "+ + "stricklers(id, name, comment, minor, medium) " + "VALUES (" + f"{self.id}, " + f"'{self._sql_format(self.name)}', " + diff --git a/src/Model/Stricklers/StricklersList.py b/src/Model/Stricklers/StricklersList.py index f1d36fa7..c8c2a925 100644 --- a/src/Model/Stricklers/StricklersList.py +++ b/src/Model/Stricklers/StricklersList.py @@ -22,6 +22,7 @@ from Model.Saved import SavedStatus from Model.Tools.PamhyrList import PamhyrModelList from Model.Stricklers.Stricklers import Stricklers + class StricklersList(PamhyrModelList): _sub_classes = [ Stricklers, @@ -36,17 +37,17 @@ class StricklersList(PamhyrModelList): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): - new = cls(status = data["status"]) + def _sql_load(cls, execute, data=None): + new = cls(status=data["status"]) new._lst = Stricklers._sql_load( execute, - data = data + data=data ) return new - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): execute("DELETE FROM stricklers") objs = self._lst @@ -57,20 +58,20 @@ class StricklersList(PamhyrModelList): return self.lst def new(self, index): - s = Stricklers(status = self._status) + s = Stricklers(status=self._status) self.insert(index, s) return s @timer - def sort(self, reverse:bool = False, key=None): - f = lambda st: st.name + def sort(self, reverse: bool = False, key=None): + def f(st): return st.name if key is not None: f = key self._lst = sorted( self._lst, - key = f, - reverse = reverse, + key=f, + reverse=reverse, ) if self._status is not None: diff --git a/src/Model/Study.py b/src/Model/Study.py index f022a195..597b68e2 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -33,12 +33,13 @@ from Checker.Study import * logger = logging.getLogger() + class Study(SQLModel): _sub_classes = [ River, ] - def __init__(self, filename = None, init_new = True): + def __init__(self, filename=None, init_new=True): # Metadata self._version = "0.0.4" self.creation_date = datetime.now() @@ -46,7 +47,7 @@ class Study(SQLModel): self.last_save_date = datetime.now() self._filename = filename - super(Study, self).__init__(filename = filename) + super(Study, self).__init__(filename=filename) self.status = SavedStatus() @@ -59,9 +60,9 @@ class Study(SQLModel): if init_new: # Study data - self._river = River(status = self.status) + self._river = River(status=self.status) else: - self._init_db_file(filename, is_new = False) + self._init_db_file(filename, is_new=False) self._old_save_id = 0 @@ -128,7 +129,7 @@ class Study(SQLModel): @filename.setter def filename(self, filename): self._filename = str(filename) - self._init_db_file(filename, is_new = True) + self._init_db_file(filename, is_new=True) self.status.modified() @property @@ -139,7 +140,7 @@ class Study(SQLModel): self._time_system = "time" self.status.modified() - def use_date(self, date:datetime): + def use_date(self, date: datetime): self._time_system = "date" self._date = date self.status.modified() @@ -158,7 +159,7 @@ class Study(SQLModel): # return cls() @classmethod - def new(cls, name, description, date = None): + def new(cls, name, description, date=None): me = cls() me.name = name me.description = description @@ -178,14 +179,16 @@ class Study(SQLModel): def _create(self): # Info (metadata) - self.execute("CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") + self.execute( + "CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") self.execute( f"INSERT INTO info VALUES ('version', '{self._sql_format(self._version)}')", - commit = True + commit=True ) self.execute("INSERT INTO info VALUES ('name', '')") self.execute("INSERT INTO info VALUES ('description', '')") - self.execute(f"INSERT INTO info VALUES ('time_system', '{self._time_system}')") + self.execute( + f"INSERT INTO info VALUES ('time_system', '{self._time_system}')") self.execute( f"INSERT INTO info VALUES ('date', " + "'{self._date.timestamp()}')" @@ -213,7 +216,8 @@ class Study(SQLModel): logger.info("Update database") if self._update_submodel(version[0]): - self.execute(f"UPDATE info SET value='{self._version}' WHERE key='version'") + self.execute( + f"UPDATE info SET value='{self._version}' WHERE key='version'") return True logger.info("TODO: update failed") @@ -221,42 +225,52 @@ class Study(SQLModel): @classmethod def _load(cls, filename): - new = cls(init_new = False, filename = filename) + new = cls(init_new=False, filename=filename) # TODO: Load metadata new.name = new.execute("SELECT value FROM info WHERE key='name'")[0] - new.description = new.execute("SELECT value FROM info WHERE key='description'")[0] - new._time_system = new.execute("SELECT value FROM info WHERE key='time_system'")[0] + new.description = new.execute( + "SELECT value FROM info WHERE key='description'")[0] + new._time_system = new.execute( + "SELECT value FROM info WHERE key='time_system'")[0] new._date = datetime.fromtimestamp( float(new.execute("SELECT value FROM info WHERE key='date'")[0]) ) new.creation_date = datetime.fromtimestamp( - float(new.execute("SELECT value FROM info WHERE key='creation_date'")[0]) + float(new.execute( + "SELECT value FROM info WHERE key='creation_date'")[0]) ) new.last_save_date = datetime.fromtimestamp( - float(new.execute("SELECT value FROM info WHERE key='last_save_date'")[0]) + float(new.execute( + "SELECT value FROM info WHERE key='last_save_date'")[0]) ) # Load river data new._river = River._sql_load( lambda sql: new.execute( sql, - fetch_one = False, - commit = True + fetch_one=False, + commit=True ), - data = {"status": new.status} + data={"status": new.status} ) return new def _save(self): - self.execute(f"UPDATE info SET value='{self._sql_format(self.name)}' WHERE key='name'") - self.execute(f"UPDATE info SET value='{self._sql_format(self.description)}' WHERE key='description'") - self.execute(f"UPDATE info SET value='{self._time_system}' WHERE key='time_system'") - self.execute(f"UPDATE info SET value='{timestamp(self._date)}' WHERE key='date'") - self.execute(f"UPDATE info SET value='{timestamp(self.creation_date)}' WHERE key='creation_date'") - self.execute(f"UPDATE info SET value='{timestamp(self.last_save_date)}' WHERE key='last_save_date'") + self.execute( + f"UPDATE info SET value='{self._sql_format(self.name)}' WHERE key='name'") + self.execute( + f"UPDATE info SET value='{self._sql_format(self.description)}' WHERE key='description'") + self.execute( + f"UPDATE info SET value='{self._time_system}' WHERE key='time_system'") + self.execute( + f"UPDATE info SET value='{timestamp(self._date)}' WHERE key='date'") + self.execute( + f"UPDATE info SET value='{timestamp(self.creation_date)}' WHERE key='creation_date'") + self.execute( + f"UPDATE info SET value='{timestamp(self.last_save_date)}' WHERE key='last_save_date'") self._save_submodel([self._river]) self.commit() diff --git a/src/Model/Tools/PamhyrDB.py b/src/Model/Tools/PamhyrDB.py index 73174f94..803cd05c 100644 --- a/src/Model/Tools/PamhyrDB.py +++ b/src/Model/Tools/PamhyrDB.py @@ -28,10 +28,12 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() # Top level model class + + class SQLModel(SQL): _sub_classes = [] - def _init_db_file(self, db, is_new = True): + def _init_db_file(self, db, is_new=True): exists = Path(db).exists() if exists and is_new: @@ -48,15 +50,14 @@ class SQLModel(SQL): self._update() # Update db scheme if necessary # self._load() # Load data - - def __init__(self, filename = None): + def __init__(self, filename=None): self._db = None def _create_submodel(self): - fn = lambda sql: self.execute( + def fn(sql): return self.execute( sql, - fetch_one = False, - commit = False + fetch_one=False, + commit=False ) for cls in self._sub_classes: @@ -69,10 +70,10 @@ class SQLModel(SQL): raise NotImplementedMethodeError(self, self._create) def _update_submodel(self, version): - fn = lambda sql: self.execute( + def fn(sql): return self.execute( sql, - fetch_one = False, - commit = False + fetch_one=False, + commit=False ) ok = True @@ -85,11 +86,11 @@ class SQLModel(SQL): def _update(self): raise NotImplementedMethodeError(self, self._update) - def _save_submodel(self, objs, data = None): - fn = lambda sql: self.execute( + def _save_submodel(self, objs, data=None): + def fn(sql): return self.execute( sql, - fetch_one = False, - commit = False + fetch_one=False, + commit=False ) ok = True @@ -103,10 +104,12 @@ class SQLModel(SQL): raise NotImplementedMethodeError(self, self._save) @classmethod - def _load(cls, filename = None): + def _load(cls, filename=None): raise NotImplementedMethodeError(cls, cls._load) # Sub model class + + class SQLSubModel(object): _sub_classes = [] @@ -154,7 +157,7 @@ class SQLSubModel(object): raise NotImplementedMethodeError(cls, cls._sql_update) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): """Load instance of this class from SQL data base Args: @@ -166,11 +169,11 @@ class SQLSubModel(object): """ raise NotImplementedMethodeError(cls, cls._sql_load) - def _save_submodel(self, execute, objs, data = None): + def _save_submodel(self, execute, objs, data=None): for o in objs: - o._sql_save(execute, data = data) + o._sql_save(execute, data=data) - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): """Save class data to data base Args: diff --git a/src/Model/Tools/PamhyrDict.py b/src/Model/Tools/PamhyrDict.py index f1652516..5d7dc537 100644 --- a/src/Model/Tools/PamhyrDict.py +++ b/src/Model/Tools/PamhyrDict.py @@ -30,7 +30,7 @@ logger = logging.getLogger() class PamhyrModelDict(SQLSubModel): _sub_classes = [] - def __init__(self, status = None): + def __init__(self, status=None): super(PamhyrModelDict, self).__init__() self._status = status @@ -46,10 +46,10 @@ class PamhyrModelDict(SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): raise NotImplementedMethodeError(cls, cls._sql_load) - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): raise NotImplementedMethodeError(self, self._sql_save) def __len__(self): diff --git a/src/Model/Tools/PamhyrList.py b/src/Model/Tools/PamhyrList.py index 9a83bdb0..57160e77 100644 --- a/src/Model/Tools/PamhyrList.py +++ b/src/Model/Tools/PamhyrList.py @@ -26,11 +26,12 @@ from Model.Except import NotImplementedMethodeError logger = logging.getLogger() + class PamhyrModelList(SQLSubModel): _sub_classes = [ ] - def __init__(self, status = None): + def __init__(self, status=None): super(PamhyrModelList, self).__init__() self._status = status @@ -49,10 +50,10 @@ class PamhyrModelList(SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): raise NotImplementedMethodeError(cls, cls._sql_load) - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): raise NotImplementedMethodeError(self, self._sql_save) ################ @@ -142,7 +143,7 @@ class PamhyrModelListWithTab(SQLSubModel): _sub_classes = [ ] - def __init__(self, status = None): + def __init__(self, status=None): super(PamhyrModelListWithTab, self).__init__() self._status = status @@ -163,10 +164,10 @@ class PamhyrModelListWithTab(SQLSubModel): return cls._update_submodel(execute, version) @classmethod - def _sql_load(cls, execute, data = None): + def _sql_load(cls, execute, data=None): raise NotImplementedMethodeError(cls, cls._sql_load) - def _sql_save(self, execute, data = None): + def _sql_save(self, execute, data=None): raise NotImplementedMethodeError(self, self._sql_save) ################ diff --git a/src/Scripts/AScript.py b/src/Scripts/AScript.py index 032f9560..66037418 100644 --- a/src/Scripts/AScript.py +++ b/src/Scripts/AScript.py @@ -18,6 +18,7 @@ from Model.Except import NotImplementedMethodeError + class AScript(object): name = "" description = "" diff --git a/src/Scripts/P3DST.py b/src/Scripts/P3DST.py index 55a99928..62d0f88f 100644 --- a/src/Scripts/P3DST.py +++ b/src/Scripts/P3DST.py @@ -29,6 +29,7 @@ from Model.Geometry.Reach import Reach logger = logging.getLogger() + class Script3DST(AScript): name = "3DST" description = "Display a 3D plot of a river reach from ST file" @@ -82,7 +83,7 @@ class Script3DST(AScript): try: status = SavedStatus() - my_reach = Reach(status = status) + my_reach = Reach(status=status) my_reach.import_geometry(st_file) my_reach.compute_guidelines() diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index d71f0472..1b070f39 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -37,12 +37,14 @@ from Model.Results.River.River import River, Reach, Profile logger = logging.getLogger() + class STATUS(Enum): NOT_LAUNCHED = -1 STOPED = 0 RUNNING = 1 PAUSED = 5 + class AbstractSolver(object): _type = "" @@ -155,7 +157,7 @@ class AbstractSolver(object): # Export # ########## - def export(self, study, repertory, qlog = None): + def export(self, study, repertory, qlog=None): raise NotImplementedMethodeError(self, self.export) def cmd_args(self, study): @@ -190,8 +192,8 @@ class AbstractSolver(object): ########### @timer - def results(self, study, repertory, qlog = None): - results = Results(study = study) + def results(self, study, repertory, qlog=None): + results = Results(study=study) return results ####### @@ -206,7 +208,7 @@ class AbstractSolver(object): ) ) - def _format_command(self, study, cmd, path = ""): + def _format_command(self, study, cmd, path=""): """Format command line Args: @@ -328,14 +330,15 @@ class AbstractSolver(object): self._run_next(study) - def run(self, study, process = None, output_queue = None): + def run(self, study, process=None, output_queue=None): if process is not None: self._process = process if output_queue is not None: self._output = output_queue self._process.readyRead.connect(self._data_ready) - self._process.finished.connect(lambda c, s: self._finished(study, c, s)) + self._process.finished.connect( + lambda c, s: self._finished(study, c, s)) self._runs = [ self.run_input_data_fomater, @@ -356,7 +359,7 @@ class AbstractSolver(object): self._status = STATUS.STOPED return True - def start(self, study, process = None): + def start(self, study, process=None): if _signal: # Solver is PAUSED, so continue execution if self._status == STATUS.PAUSED: diff --git a/src/Solver/GenericSolver.py b/src/Solver/GenericSolver.py index 9e588bae..bc5a9f6b 100644 --- a/src/Solver/GenericSolver.py +++ b/src/Solver/GenericSolver.py @@ -20,6 +20,7 @@ from Solver.ASolver import ( AbstractSolver, STATUS ) + class GenericSolver(AbstractSolver): _type = "generic" diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 1cad3f68..15d3979b 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -30,6 +30,7 @@ from Model.Results.River.River import River, Reach, Profile logger = logging.getLogger() + def mage_file_open(filepath, mode): f = open(filepath, mode) @@ -39,6 +40,7 @@ def mage_file_open(filepath, mode): return f + class Mage(AbstractSolver): _type = "mage" @@ -51,7 +53,6 @@ class Mage(AbstractSolver): self._cmd_solver = "@path @input -o @output" self._cmd_output = "" - @classmethod def default_parameters(cls): lst = super(Mage, cls).default_parameters() @@ -87,8 +88,8 @@ class Mage(AbstractSolver): @classmethod def checkers(cls): lst = [ - MageNetworkGraphChecker(connectivity = True), - MageNetworkGraphChecker(connectivity = False) + MageNetworkGraphChecker(connectivity=True), + MageNetworkGraphChecker(connectivity=False) ] return lst @@ -210,7 +211,7 @@ class Mage(AbstractSolver): v1 = d[1] if t in ["HYD", "QSO", "LIM"]: - v0 /= 60 # Convert first column to minute + v0 /= 60 # Convert first column to minute f.write(f"{v0:10}{v1:10}\n") @@ -391,13 +392,12 @@ class Mage(AbstractSolver): if EXT in ["GRA"]: f.write(f"{EXT} {file}\n") - @timer - def export(self, study, repertory, qlog = None): + def export(self, study, repertory, qlog=None): self._study = study name = study.name.replace(" ", "_") - self._export_ST(study, repertory, qlog, name = name) + self._export_ST(study, repertory, qlog, name=name) return True @@ -405,12 +405,12 @@ class Mage(AbstractSolver): # RESULTS # ########### - def read_bin(self, study, repertory, results, qlog = None, name="0"): + def read_bin(self, study, repertory, results, qlog=None, name="0"): return @timer - def results(self, study, repertory, qlog = None, name="0"): - results = Results(study = study) + def results(self, study, repertory, qlog=None, name="0"): + results = Results(study=study) self.read_bin(study, repertory, results, qlog, name=name) @@ -420,6 +420,7 @@ class Mage(AbstractSolver): # MAGE 7 # ########## + class Mage7(Mage): _type = "mage7" @@ -438,6 +439,7 @@ class Mage7(Mage): # MAGE 8 # ########## + class Mage8(Mage): _type = "mage8" @@ -484,7 +486,7 @@ class Mage8(Mage): return l @timer - def _export_PAR(self, study, repertory, qlog = None, name = "0"): + def _export_PAR(self, study, repertory, qlog=None, name="0"): files = [] if qlog is not None: @@ -511,7 +513,7 @@ class Mage8(Mage): return files @timer - def _export_NET(self, study, repertory, qlog = None, name="0"): + def _export_NET(self, study, repertory, qlog=None, name="0"): files = [] if qlog is not None: @@ -579,9 +581,8 @@ class Mage8(Mage): return files - @timer - def export(self, study, repertory, qlog = None, name="0"): + def export(self, study, repertory, qlog=None, name="0"): self._study = study name = study.name.replace(" ", "_") @@ -590,7 +591,8 @@ class Mage8(Mage): files = self._export_ST(study, repertory, qlog, name=name) files = files + self._export_PAR(study, repertory, qlog, name=name) files = files + self._export_NET(study, repertory, qlog, name=name) - files = files + self._export_bound_cond(study, repertory, qlog, name=name) + files = files + \ + 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) self._export_REP(study, repertory, files, qlog, name=name) @@ -602,17 +604,22 @@ class Mage8(Mage): ########### @timer - def read_bin(self, study, repertory, results, qlog = None, name="0"): + def read_bin(self, study, repertory, results, qlog=None, name="0"): fname = os.path.join(repertory, f"{name}.BIN") logger.info(f"read_bin: Start reading '{fname}' ...") with mage_file_open(fname, "r") as f: - newline = lambda: np.fromfile(f, dtype=np.int32, count=1) - endline = lambda: np.fromfile(f, dtype=np.int32, count=1) + def newline(): return np.fromfile(f, dtype=np.int32, count=1) + def endline(): return np.fromfile(f, dtype=np.int32, count=1) - read_int = lambda size: np.fromfile(f, dtype=np.int32, count=size) - read_float = lambda size: np.fromfile(f, dtype=np.float32, count=size) - read_float64 = lambda size: np.fromfile(f, dtype=np.float64, count=size) + def read_int(size): return np.fromfile( + f, dtype=np.int32, count=size) + + def read_float(size): return np.fromfile( + f, dtype=np.float32, count=size) + + def read_float64(size): return np.fromfile( + f, dtype=np.float64, count=size) # Meta data (1st line) newline() @@ -686,7 +693,7 @@ class Mage8(Mage): # Data newline() - ip_to_r = lambda i: iprofiles[ + def ip_to_r(i): return iprofiles[ next( filter( lambda k: k[0] <= i <= k[1], @@ -694,14 +701,15 @@ class Mage8(Mage): ) ) ] - ip_to_ri = lambda r, i: i - reach_offset[r] + def ip_to_ri(r, i): return i - reach_offset[r] ts = set() end = False while not end: n = read_int(1)[0] timestamp = read_float64(1)[0] - key = bytearray(np.fromfile(f, dtype=np.byte, count=1)).decode() + key = bytearray(np.fromfile( + f, dtype=np.byte, count=1)).decode() data = read_float(n) logger.debug(f"read_bin: timestamp = {timestamp} sec") @@ -725,7 +733,7 @@ class Mage8(Mage): logger.info(f"read_bin: ... end with {len(ts)} timestamp read") @timer - def read_gra(self, study, repertory, results, qlog = None, name="0"): + def read_gra(self, study, repertory, results, qlog=None, name="0"): if not study.river.has_sediment(): return @@ -733,12 +741,17 @@ class Mage8(Mage): logger.info(f"read_gra: Start reading '{fname}' ...") with mage_file_open(fname, "r") as f: - newline = lambda: np.fromfile(f, dtype=np.int32, count=1) - endline = lambda: np.fromfile(f, dtype=np.int32, count=1) + def newline(): return np.fromfile(f, dtype=np.int32, count=1) + def endline(): return np.fromfile(f, dtype=np.int32, count=1) - read_int = lambda size: np.fromfile(f, dtype=np.int32, count=size) - read_float = lambda size: np.fromfile(f, dtype=np.float32, count=size) - read_float64 = lambda size: np.fromfile(f, dtype=np.float64, count=size) + def read_int(size): return np.fromfile( + f, dtype=np.int32, count=size) + + def read_float(size): return np.fromfile( + f, dtype=np.float32, count=size) + + def read_float64(size): return np.fromfile( + f, dtype=np.float64, count=size) # Meta data (1st line) newline() @@ -810,7 +823,7 @@ class Mage8(Mage): endline() # Data - ip_to_r = lambda i: iprofiles[ + def ip_to_r(i): return iprofiles[ next( filter( lambda k: k[0] <= i <= k[1], @@ -818,7 +831,7 @@ class Mage8(Mage): ) ) ] - ip_to_ri = lambda r, i: i - reach_offset[r] + def ip_to_ri(r, i): return i - reach_offset[r] ts = set() ind = 0 @@ -878,11 +891,11 @@ class Mage8(Mage): logger.info(f"read_gra: ... end with {len(ts)} timestamp read") @timer - def results(self, study, repertory, qlog = None): + def results(self, study, repertory, qlog=None): self._study = study name = study.name.replace(" ", "_") - results = super(Mage8, self).results(study, repertory, qlog, name = name) - self.read_gra(study, repertory, results, qlog, name = name) + results = super(Mage8, self).results(study, repertory, qlog, name=name) + self.read_gra(study, repertory, results, qlog, name=name) return results diff --git a/src/View/About/Window.py b/src/View/About/Window.py index 218869b5..7798729e 100644 --- a/src/View/About/Window.py +++ b/src/View/About/Window.py @@ -26,6 +26,7 @@ from PyQt5.QtCore import QCoreApplication _translate = QCoreApplication.translate logger = logging.getLogger() + class AboutWindow(PamhyrDialog): _pamhyr_ui = "about" _pamhyr_name = "About" @@ -38,13 +39,13 @@ class AboutWindow(PamhyrDialog): ) ) - def __init__(self, study = None, config = None, parent=None): + def __init__(self, study=None, config=None, parent=None): super(AboutWindow, self).__init__( - title = _translate("About", "About"), - study = study, - config = config, - options = [], - parent = parent + title=_translate("About", "About"), + study=study, + config=config, + options=[], + parent=parent ) # Version diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index e5e80c1e..3c1b8808 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -33,16 +33,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class Plot(PamhyrPlot): - def __init__(self, mode = "time", data=None, + def __init__(self, mode="time", data=None, trad=None, canvas=None, toolbar=None, parent=None): super(Plot, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self._table_headers = self._trad.get_dict("table_headers") @@ -72,8 +73,8 @@ class Plot(PamhyrPlot): map( lambda v: str( datetime.fromtimestamp(v[0]) - t0 - ).split(",")[0]\ - .replace("days", _translate("BoundaryCondition", "days"))\ + ).split(",")[0] + .replace("days", _translate("BoundaryCondition", "days")) .replace("day", _translate("BoundaryCondition", "day")), fx ) @@ -88,7 +89,6 @@ class Plot(PamhyrPlot): self.canvas.axes.set_xticks(ticks=xx, labels=xt, rotation=45) - @timer def draw(self): self.canvas.axes.cla() @@ -122,7 +122,7 @@ class Plot(PamhyrPlot): self.canvas.axes.autoscale_view(True, True, True) self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() - #self.toolbar.update() + # self.toolbar.update() self._init = True diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 7f86fc8a..75e68131 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -51,6 +51,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class TableModel(PamhyrTableModel): def data(self, index, role): if role == Qt.TextAlignmentRole: diff --git a/src/View/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py index b30331a1..14c2b982 100644 --- a/src/View/BoundaryCondition/Edit/UndoCommand.py +++ b/src/View/BoundaryCondition/Edit/UndoCommand.py @@ -29,6 +29,7 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition logger = logging.getLogger() + class SetDataCommand(QUndoCommand): def __init__(self, data, index, column, new_value): QUndoCommand.__init__(self) @@ -46,6 +47,7 @@ class SetDataCommand(QUndoCommand): def redo(self): self._data._set_i_c_v(self._index, self._column, self._new) + class SetMetaDataCommand(QUndoCommand): def __init__(self, data, column, new_value): QUndoCommand.__init__(self) @@ -89,6 +91,7 @@ class AddCommand(QUndoCommand): else: self._data.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, data, rows): QUndoCommand.__init__(self) @@ -108,6 +111,7 @@ class DelCommand(QUndoCommand): def redo(self): self._data.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, data, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 5661d6bc..162459ed 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -53,6 +53,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class WD50Sigma(PamhyrWidget): _pamhyr_ui = "d50sigma" @@ -94,6 +95,7 @@ class WD50Sigma(PamhyrWidget): def valueChangedSigma(self, value): self.sigmaChanged.emit(value) + class EditBoundaryConditionWindow(PamhyrWindow): _pamhyr_ui = "EditBoundaryConditions" _pamhyr_name = "Edit Boundary Conditions" @@ -115,11 +117,11 @@ class EditBoundaryConditionWindow(PamhyrWindow): ) super(EditBoundaryConditionWindow, self).__init__( - title = name, - study = study, - config = config, - trad = trad, - parent = parent + title=name, + study=study, + config=config, + trad=trad, + parent=parent ) self.setup_table() @@ -132,7 +134,7 @@ class EditBoundaryConditionWindow(PamhyrWindow): if self._is_solid: layout = self.find(QVBoxLayout, "verticalLayout_table") - self._d50sigma = WD50Sigma(parent = self) + self._d50sigma = WD50Sigma(parent=self) layout.addWidget(self._d50sigma) self._d50sigma.set_d50(self._data.d50) @@ -145,22 +147,22 @@ class EditBoundaryConditionWindow(PamhyrWindow): headers[h] = table_headers[h] self._delegate_time = PamhyrExTimeDelegate( - data = self._data, - mode = self._study.time_system, - parent = self + data=self._data, + mode=self._study.time_system, + parent=self ) table = self.find(QTableView, "tableView") self._table = TableModel( - table_view = table, - table_headers = headers, - editable_headers = self._data.header, - delegates = { + table_view=table, + table_headers=headers, + editable_headers=self._data.header, + delegates={ "time": self._delegate_time, }, - data = self._data, - undo = self._undo_stack, - opt_data = self._study.time_system + data=self._data, + undo=self._undo_stack, + opt_data=self._study.time_system ) table.setModel(self._table) @@ -178,11 +180,11 @@ class EditBoundaryConditionWindow(PamhyrWindow): self.verticalLayout.addWidget(self.canvas) self.plot = Plot( - canvas = self.canvas, - data = self._data, - mode = self._study.time_system, - trad = self._trad, - toolbar = self.toolbar, + canvas=self.canvas, + data=self._data, + mode=self._study.time_system, + trad=self._trad, + toolbar=self.toolbar, ) self.plot.draw() @@ -270,7 +272,6 @@ class EditBoundaryConditionWindow(PamhyrWindow): self._table.move_down(row) self.plot.update() - def _copy(self): rows = self.index_selected_rows() diff --git a/src/View/BoundaryCondition/Edit/translate.py b/src/View/BoundaryCondition/Edit/translate.py index d97ac6de..9f54ce2a 100644 --- a/src/View/BoundaryCondition/Edit/translate.py +++ b/src/View/BoundaryCondition/Edit/translate.py @@ -24,6 +24,7 @@ from View.BoundaryCondition.translate import BCTranslate _translate = QCoreApplication.translate + class BCETranslate(BCTranslate): def __init__(self): super(BCETranslate, self).__init__() diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py index 77141dff..3a5afd8f 100644 --- a/src/View/BoundaryCondition/Table.py +++ b/src/View/BoundaryCondition/Table.py @@ -52,6 +52,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate + class ComboBoxDelegate(QItemDelegate): def __init__(self, data=None, mode="type", tab="", trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) @@ -157,20 +158,21 @@ class TableModel(PamhyrTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._lst, self._tab,row, value + self._lst, self._tab, row, value ) ) elif self._headers[column] == "type": - key = next(k for k, v in self._long_types.items() if v == value) + key = next(k for k, v in self._long_types.items() + if v == value) self._undo.push( SetTypeCommand( - self._lst, self._tab,row, BC_types[key] + self._lst, self._tab, row, BC_types[key] ) ) elif self._headers[column] == "node": self._undo.push( SetNodeCommand( - self._lst, self._tab,row, self._data.node(value) + self._lst, self._tab, row, self._data.node(value) ) ) except Exception as e: @@ -185,7 +187,7 @@ class TableModel(PamhyrTableModel): self._undo.push( AddCommand( - self._lst, self._tab,row + self._lst, self._tab, row ) ) diff --git a/src/View/BoundaryCondition/UndoCommand.py b/src/View/BoundaryCondition/UndoCommand.py index bbf8cb2c..356d854b 100644 --- a/src/View/BoundaryCondition/UndoCommand.py +++ b/src/View/BoundaryCondition/UndoCommand.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList + class SetNameCommand(QUndoCommand): def __init__(self, bcs, tab, index, new_value): QUndoCommand.__init__(self) @@ -42,6 +43,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._bcs.get(self._tab, self._index).name = self._new + class SetNodeCommand(QUndoCommand): def __init__(self, bcs, tab, index, node): QUndoCommand.__init__(self) @@ -58,6 +60,7 @@ class SetNodeCommand(QUndoCommand): def redo(self): self._bcs.get(self._tab, self._index).node = self._new + class SetTypeCommand(QUndoCommand): def __init__(self, bcs, tab, index, _type): QUndoCommand.__init__(self) @@ -76,6 +79,7 @@ class SetTypeCommand(QUndoCommand): def redo(self): self._bcs.set(self._tab, self._index, self._new) + class AddCommand(QUndoCommand): def __init__(self, bcs, tab, index): QUndoCommand.__init__(self) @@ -94,6 +98,7 @@ class AddCommand(QUndoCommand): else: self._bcs.insert(self._tab, self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, bcs, tab, rows): QUndoCommand.__init__(self) @@ -114,6 +119,7 @@ class DelCommand(QUndoCommand): def redo(self): self._bcs.delete_i(self._tab, self._rows) + class SortCommand(QUndoCommand): def __init__(self, bcs, tab, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index 99758c5a..9d30532e 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -62,6 +62,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class BoundaryConditionWindow(PamhyrWindow): _pamhyr_ui = "BoundaryConditions" _pamhyr_name = "Boundary conditions" @@ -70,10 +71,10 @@ class BoundaryConditionWindow(PamhyrWindow): name = self._pamhyr_name + " - " + study.name super(BoundaryConditionWindow, self).__init__( - title = name, - study = study, - config = config, - trad = BCTranslate(), + title=name, + study=study, + config=config, + trad=BCTranslate(), parent=parent ) @@ -90,33 +91,33 @@ class BoundaryConditionWindow(PamhyrWindow): for t in ["liquid", "solid", "suspenssion"]: self._delegate_type = ComboBoxDelegate( - trad = self._trad, - data = self._study.river, - mode = "type", - tab = t, + trad=self._trad, + data=self._study.river, + mode="type", + tab=t, parent=self ) self._delegate_node = ComboBoxDelegate( - trad = self._trad, - data = self._study.river, - mode = "node", - tab = t, + trad=self._trad, + data=self._study.river, + mode="node", + tab=t, parent=self ) table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( - table_view = table, - table_headers = self._trad.get_dict("table_headers"), - editable_headers = ["name", "type", "node"], - delegates = { + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "type", "node"], + delegates={ "type": self._delegate_type, "node": self._delegate_node, }, - trad = self._trad, - data = self._study.river, - undo = self._undo_stack, - opt_data = t, + trad=self._trad, + data=self._study.river, + undo=self._undo_stack, + opt_data=t, ) table.setModel(self._table[t]) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -126,7 +127,7 @@ class BoundaryConditionWindow(PamhyrWindow): def setup_graph(self): self.graph_widget = GraphWidget( self._study.river, - min_size=None, size=(200,200), + min_size=None, size=(200, 200), only_display=True, parent=self ) @@ -215,7 +216,7 @@ class BoundaryConditionWindow(PamhyrWindow): for row in rows: win = self.sub_win_filter_first( "Edit boundary condition", - contain = [f"({self._bcs.get(tab, row).id})"] + contain=[f"({self._bcs.get(tab, row).id})"] ) if win is None: diff --git a/src/View/BoundaryCondition/translate.py b/src/View/BoundaryCondition/translate.py index 00077f5f..d16c644e 100644 --- a/src/View/BoundaryCondition/translate.py +++ b/src/View/BoundaryCondition/translate.py @@ -37,6 +37,7 @@ BC_types = { "SL": Solid, } + class BCTranslate(PamhyrTranslate): def __init__(self): super(BCTranslate, self).__init__() diff --git a/src/View/CheckList/Translate.py b/src/View/CheckList/Translate.py index b3ce856d..d046365d 100644 --- a/src/View/CheckList/Translate.py +++ b/src/View/CheckList/Translate.py @@ -22,6 +22,7 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class CheckListTranslate(PamhyrTranslate): def __init__(self): super(CheckListTranslate, self).__init__() diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index baf99983..1e8fb5d4 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -44,13 +44,14 @@ from View.CheckList.Translate import CheckListTranslate _translate = QCoreApplication.translate + class CheckListWindow(PamhyrWindow): _pamhyr_ui = "CheckList" _pamhyr_name = "Check list" signalStatus = pyqtSignal(str) - def __init__(self, autorun:bool=True, + def __init__(self, autorun: bool = True, study=None, config=None, solver=None, parent=None): self._autorun = autorun @@ -59,12 +60,12 @@ class CheckListWindow(PamhyrWindow): name = self._pamhyr_name + " - " + study.name super(CheckListWindow, self).__init__( - title = name, - study = study, - config = config, - trad = CheckListTranslate(), - options = [], - parent = parent + title=name, + study=study, + config=config, + trad=CheckListTranslate(), + options=[], + parent=parent ) self._checker_list = ( @@ -81,9 +82,9 @@ class CheckListWindow(PamhyrWindow): def setup_table(self): table = self.find(QTableView, f"tableView") self._table = TableModel( - table_view = table, - table_headers = self._trad.get_dict("table_headers"), - data = self._checker_list, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + data=self._checker_list, ) def setup_progress_bar(self): @@ -120,7 +121,8 @@ class CheckListWindow(PamhyrWindow): def _compute_status(self): ok = len(list(filter(lambda c: c.is_ok(), self._checker_list))) - warning = len(list(filter(lambda c: c.is_warning(), self._checker_list))) + warning = len( + list(filter(lambda c: c.is_warning(), self._checker_list))) error = len(list(filter(lambda c: c.is_error(), self._checker_list))) return ok, warning, error @@ -164,7 +166,7 @@ class CheckListWindow(PamhyrWindow): self.update_statusbar() - def update(self, key:str): + def update(self, key: str): if key == "start": self.start_compute() self.info_compute("Starting ...") @@ -187,4 +189,4 @@ class CheckListWindow(PamhyrWindow): def accept(self): self._parent.solver_log(self._solver) - #self.end() + # self.end() diff --git a/src/View/CheckList/Worker.py b/src/View/CheckList/Worker.py index aacd9393..834d8975 100644 --- a/src/View/CheckList/Worker.py +++ b/src/View/CheckList/Worker.py @@ -22,6 +22,7 @@ from PyQt5.QtCore import ( Qt, QObject, pyqtSlot, pyqtSignal ) + class Worker(QObject): signalStatus = pyqtSignal(str) diff --git a/src/View/Configure/Solver/Window.py b/src/View/Configure/Solver/Window.py index 369ff40a..893def20 100644 --- a/src/View/Configure/Solver/Window.py +++ b/src/View/Configure/Solver/Window.py @@ -24,6 +24,7 @@ from PyQt5.QtWidgets import ( QPushButton, ) + class ConfigureSolverWindow(PamhyrDialog): _pamhyr_ui = "ConfigureAddSolverDialog" _pamhyr_name = "Add/Edit Solver" @@ -35,9 +36,9 @@ class ConfigureSolverWindow(PamhyrDialog): name = "Add a new Solver" super(ConfigureSolverWindow, self).__init__( - title = name, - config = config, - options = [], + title=name, + config=config, + options=[], parent=parent ) @@ -67,16 +68,19 @@ class ConfigureSolverWindow(PamhyrDialog): # File button buttons = { "pushButton_input": (lambda: self.file_dialog( - select_file = True, - callback = lambda f: self.set_line_edit_text("lineEdit_input", f[0]) + select_file=True, + callback=lambda f: self.set_line_edit_text( + "lineEdit_input", f[0]) )), "pushButton_solver": (lambda: self.file_dialog( - select_file = True, - callback = lambda f: self.set_line_edit_text("lineEdit_solver", f[0]) + select_file=True, + callback=lambda f: self.set_line_edit_text( + "lineEdit_solver", f[0]) )), "pushButton_output": (lambda: self.file_dialog( - select_file = True, - callback = lambda f: self.set_line_edit_text("lineEdit_output", f[0]) + select_file=True, + callback=lambda f: self.set_line_edit_text( + "lineEdit_output", f[0]) )), } @@ -92,9 +96,11 @@ class ConfigureSolverWindow(PamhyrDialog): else: # Build new solver from selected type stype = self.get_combobox_text("comboBox_solver") - self.data = solver_type_list[stype](self.get_line_edit_text("lineEdit_name")) + self.data = solver_type_list[stype]( + self.get_line_edit_text("lineEdit_name")) - self.data.description = self.get_line_edit_text("lineEdit_description") + self.data.description = self.get_line_edit_text( + "lineEdit_description") self.data.set_input( self.get_line_edit_text("lineEdit_input"), self.get_line_edit_text("lineEdit_input_cmd") diff --git a/src/View/Configure/Translate.py b/src/View/Configure/Translate.py index bed6a713..5355d8a5 100644 --- a/src/View/Configure/Translate.py +++ b/src/View/Configure/Translate.py @@ -22,6 +22,7 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class ConfigureTranslate(PamhyrTranslate): def __init__(self): super(ConfigureTranslate, self).__init__() diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index b07304fa..f380e868 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -49,6 +49,7 @@ from Solver.Solvers import solver_long_name logger = logging.getLogger() + class SolverTableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -83,10 +84,10 @@ class ConfigureWindow(PamhyrDialog): config = Config() super(ConfigureWindow, self).__init__( - title = self._pamhyr_name, - config = config, - trad = ConfigureTranslate(), - options = [], + title=self._pamhyr_name, + config=config, + trad=ConfigureTranslate(), + options=[], parent=parent ) @@ -101,38 +102,43 @@ class ConfigureWindow(PamhyrDialog): def setup_solver(self): table = self.find(QTableView, "tableView_solver") self.solver_table_model = SolverTableModel( - table_view = table, - table_headers = self._trad.get_dict("table_headers"), - data = self._config.solvers + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + data=self._config.solvers ) def setup_stricklers(self): table = self.find(QTableView, f"tableView_stricklers") self._stricklers = deepcopy(self._config.stricklers) self._stricklers_table = TableModel( - table_view = table, - table_headers = self._trad_stricklers.get_dict("table_headers"), - editable_headers = ["name", "comment", "minor", "medium"], - data = self._stricklers, - undo = self._undo_stack, + table_view=table, + table_headers=self._trad_stricklers.get_dict("table_headers"), + editable_headers=["name", "comment", "minor", "medium"], + data=self._stricklers, + undo=self._undo_stack, ) def setup_data(self): # Meshing_Tool - self.set_line_edit_text("lineEdit_meshing_tool", self._config.meshing_tool) + self.set_line_edit_text("lineEdit_meshing_tool", + self._config.meshing_tool) # Const self.set_line_edit_text("lineEdit_segment", str(self._config.segment)) - self.set_line_edit_text("lineEdit_max_listing", str(self._config.max_listing)) + self.set_line_edit_text("lineEdit_max_listing", + str(self._config.max_listing)) # Backup self.set_check_box("checkBox_backup", self._config.backup_enable) - self.set_line_edit_text("lineEdit_backup_path", self._config.backup_path) - self.set_time_edit("timeEdit_backup_frequence", self._config.backup_frequence) + self.set_line_edit_text("lineEdit_backup_path", + self._config.backup_path) + self.set_time_edit("timeEdit_backup_frequence", + self._config.backup_frequence) self.set_spin_box("spinBox_backup_max", self._config.backup_max) # Editor - self.set_line_edit_text("lineEdit_editor_cmd", str(self._config.editor)) + self.set_line_edit_text("lineEdit_editor_cmd", + str(self._config.editor)) # Language languages = Config.languages() @@ -157,14 +163,14 @@ class ConfigureWindow(PamhyrDialog): "pushButton_stricklers_sort": self.sort_stricklers, # Others "pushButton_backup_path": lambda: self.file_dialog( - select_file = False, - callback = lambda f: self.set_line_edit_text( + select_file=False, + callback=lambda f: self.set_line_edit_text( "lineEdit_backup_path", f[0] ) ), - "pushButton_meshing_tool" : lambda: self.file_dialog( - select_file = True, - callback = lambda f: self.set_line_edit_text( + "pushButton_meshing_tool": lambda: self.file_dialog( + select_file=True, + callback=lambda f: self.set_line_edit_text( "lineEdit_meshing_tool", f[0] ) ), @@ -178,16 +184,20 @@ class ConfigureWindow(PamhyrDialog): self._config.solvers = self.solver_table_model._data.copy() # Meshing_Tool - self._config.meshing_tool = self.get_line_edit_text("lineEdit_meshing_tool") + self._config.meshing_tool = self.get_line_edit_text( + "lineEdit_meshing_tool") # Const self._config.segment = self.get_line_edit_text("lineEdit_segment") - self._config.max_listing = self.get_line_edit_text("lineEdit_max_listing") + self._config.max_listing = self.get_line_edit_text( + "lineEdit_max_listing") # Backup self._config.backup_enable = self.get_check_box("checkBox_backup") - self._config.backup_path = self.get_line_edit_text("lineEdit_backup_path") - self._config.backup_frequence = self.get_time_edit("timeEdit_backup_frequence") + self._config.backup_path = self.get_line_edit_text( + "lineEdit_backup_path") + self._config.backup_frequence = self.get_time_edit( + "timeEdit_backup_frequence") self._config.backup_max = self.get_spin_box("spinBox_backup_max") # Stricklers @@ -197,7 +207,8 @@ class ConfigureWindow(PamhyrDialog): self._config.editor = self.get_line_edit_text("lineEdit_editor_cmd") # Language - self._config.lang = Config.languages()[self.get_combobox_text("comboBox_language")] + self._config.lang = Config.languages( + )[self.get_combobox_text("comboBox_language")] self.end() @@ -219,7 +230,8 @@ class ConfigureWindow(PamhyrDialog): # Solvers def edit_solver(self): - indexes = self.find(QTableView, "tableView_solver").selectionModel().selectedRows() + indexes = self.find( + QTableView, "tableView_solver").selectionModel().selectedRows() for index in indexes: self.edit_solver = ConfigureSolverWindow( data=self.solver_table_model._data[index.row()], @@ -227,7 +239,8 @@ class ConfigureWindow(PamhyrDialog): parent=self ) if self.edit_solver.exec_(): - self.solver_table_model.change_solver(self.edit_solver.data, index) + self.solver_table_model.change_solver( + self.edit_solver.data, index) def add_solver(self): dialog_solver = ConfigureSolverWindow(parent=self) @@ -235,7 +248,8 @@ class ConfigureWindow(PamhyrDialog): self.solver_table_model.add_solver(dialog_solver.data) def remove_solver(self): - indices = self.find(QTableView, "tableView_solver").selectionModel().selectedRows() + indices = self.find( + QTableView, "tableView_solver").selectionModel().selectedRows() for index in sorted(indices): self.solver_table_model.removeRow(index) diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py index 2e99dadb..4d60a396 100644 --- a/src/View/Debug/Window.py +++ b/src/View/Debug/Window.py @@ -44,6 +44,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate + class ReplWindow(PamhyrWindow): _pamhyr_ui = "DebugRepl" _pamhyr_name = "Debug REPL" @@ -51,11 +52,11 @@ class ReplWindow(PamhyrWindow): def __init__(self, study=None, config=None, solver=None, parent=None): super(ReplWindow, self).__init__( - title = self._pamhyr_name, - study = study, - config = config, - options = [], - parent = parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) self.__debug_exec_result__ = None @@ -75,12 +76,14 @@ class ReplWindow(PamhyrWindow): def history_up(self): if self._history_ind < len(self._history): self._history_ind += 1 - self.set_plaintext_edit_text("plainTextEdit", self._history[-self._history_ind]) + self.set_plaintext_edit_text( + "plainTextEdit", self._history[-self._history_ind]) def history_down(self): if self._history_ind > 0: self._history_ind -= 1 - self.set_plaintext_edit_text("plainTextEdit", self._history[-self._history_ind]) + self.set_plaintext_edit_text( + "plainTextEdit", self._history[-self._history_ind]) else: self.set_plaintext_edit_text("plainTextEdit", "") diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py index e197e566..9f64da52 100644 --- a/src/View/Doc/Window.py +++ b/src/View/Doc/Window.py @@ -16,7 +16,8 @@ # -*- coding: utf-8 -*- -import os, sys +import os +import sys import logging import subprocess @@ -55,11 +56,11 @@ class DocWindow(PamhyrWindow): study=None, config=None, parent=None): super(DocWindow, self).__init__( - title = self._pamhyr_name, - study = study, - config = config, - options = [], - parent = parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) self._path = self._path_file(filename) @@ -70,11 +71,11 @@ class DocWindow(PamhyrWindow): def setup_connection(self): self.findChild(QAction, "action_back").triggered.connect(self.back) - self.findChild(QAction, "action_forward").triggered.connect(self.forward) + self.findChild(QAction, "action_forward").triggered.connect( + self.forward) self.findChild(QAction, "action_open").triggered.connect(self.open) - def setup_web_engine(self): vl = self.find(QVBoxLayout, "verticalLayout") self._web_view = QWebEngineView() @@ -97,5 +98,5 @@ class DocWindow(PamhyrWindow): self._web_view.forward() def open(self): - url = QUrl(f"file://{self._path}") - QDesktopServices.openUrl(url) + url = QUrl(f"file://{self._path}") + QDesktopServices.openUrl(url) diff --git a/src/View/DummyWindow.py b/src/View/DummyWindow.py index 768d6fbc..aaab178c 100644 --- a/src/View/DummyWindow.py +++ b/src/View/DummyWindow.py @@ -18,12 +18,13 @@ from View.Tools.PamhyrWindow import PamhyrWindow + class DummyWindow(PamhyrWindow): __ui = "Dummy" __name = "Pamhyr Dummy Window" def __init__(self, parent=None): super(DummyWindow, self).__init__( - title = self.__name, - parent = parent, + title=self.__name, + parent=parent, ) diff --git a/src/View/Frictions/PlotStricklers.py b/src/View/Frictions/PlotStricklers.py index c6b58dcd..89fe50f8 100644 --- a/src/View/Frictions/PlotStricklers.py +++ b/src/View/Frictions/PlotStricklers.py @@ -25,10 +25,11 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate + class PlotStricklers(PamhyrPlot): def draw_frictions(self, frictions, color="r"): lst = frictions - lst.sort(key = lambda s: s.begin_kp) + lst.sort(key=lambda s: s.begin_kp) coef = flatten( map( @@ -57,7 +58,6 @@ class PlotStricklers(PamhyrPlot): color=color, lw=1. ) - @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -77,7 +77,7 @@ class PlotStricklers(PamhyrPlot): kp = self.data.reach.get_kp() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) frictions = self.data.frictions diff --git a/src/View/Frictions/Table.py b/src/View/Frictions/Table.py index f4ad4f8e..1cf3dcc1 100644 --- a/src/View/Frictions/Table.py +++ b/src/View/Frictions/Table.py @@ -49,6 +49,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate + class ComboBoxDelegate(QItemDelegate): def __init__(self, data=None, study=None, mode="stricklers", parent=None): super(ComboBoxDelegate, self).__init__(parent) diff --git a/src/View/Frictions/UndoCommand.py b/src/View/Frictions/UndoCommand.py index ef902f07..5aba2a7c 100644 --- a/src/View/Frictions/UndoCommand.py +++ b/src/View/Frictions/UndoCommand.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from Model.Friction.Friction import Friction from Model.Friction.FrictionList import FrictionList + class SetNameCommand(QUndoCommand): def __init__(self, frictions, index, new_value): QUndoCommand.__init__(self) @@ -41,6 +42,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._frictions.get(self._index).name = self._new + class SetBeginCommand(QUndoCommand): def __init__(self, frictions, index, new_value): QUndoCommand.__init__(self) @@ -56,6 +58,7 @@ class SetBeginCommand(QUndoCommand): def redo(self): self._frictions.get(self._index).begin_kp = float(self._new) + class SetEndCommand(QUndoCommand): def __init__(self, frictions, index, new_value): QUndoCommand.__init__(self) @@ -87,6 +90,7 @@ class SetBeginStricklerCommand(QUndoCommand): def redo(self): self._frictions.get(self._index).begin_strickler = self._new + class SetEndStricklerCommand(QUndoCommand): def __init__(self, frictions, index, new_value): QUndoCommand.__init__(self) @@ -118,6 +122,7 @@ class SetEdgeCommand(QUndoCommand): def redo(self): self._frictions.get(self._index).edge = self._new + class AddCommand(QUndoCommand): def __init__(self, frictions, index, reach): QUndoCommand.__init__(self) @@ -137,6 +142,7 @@ class AddCommand(QUndoCommand): else: self._frictions.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, frictions, rows): QUndoCommand.__init__(self) @@ -156,6 +162,7 @@ class DelCommand(QUndoCommand): def redo(self): self._frictions.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, frictions, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 74d96b63..2558ab43 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -56,6 +56,7 @@ from View.Stricklers.Window import StricklersWindow logger = logging.getLogger() + class FrictionsWindow(PamhyrWindow): _pamhyr_ui = "Frictions" _pamhyr_name = "Edit frictions" @@ -75,43 +76,42 @@ class FrictionsWindow(PamhyrWindow): ) super(FrictionsWindow, self).__init__( - title = name, - study = study, - config = config, - trad = FrictionsTranslate(), - parent = parent + title=name, + study=study, + config=config, + trad=FrictionsTranslate(), + parent=parent ) self.setup_table() self.setup_graph() self.setup_connections() - def setup_table(self): self._table = {} self._delegate_stricklers = ComboBoxDelegate( - data = self._reach, - study = self._study, - mode = "stricklers", + data=self._reach, + study=self._study, + mode="stricklers", parent=self ) table = self.find(QTableView, f"tableView") self._table = TableModel( - table_view = table, - table_headers = self._trad.get_dict("table_headers"), - editable_headers = [ + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=[ "name", "begin_kp", "end_kp", "begin_strickler", "end_strickler" ], - delegates = { + delegates={ "begin_strickler": self._delegate_stricklers, "end_strickler": self._delegate_stricklers, }, - data = self._reach, - undo = self._undo_stack, - opt_data = self._study + data=self._reach, + undo=self._undo_stack, + opt_data=self._study ) table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -125,10 +125,10 @@ class FrictionsWindow(PamhyrWindow): self.plot_layout.addWidget(self.canvas) self.plot = PlotKPZ( - canvas = self.canvas, - data = self._reach.reach, - toolbar = None, - display_current = False + canvas=self.canvas, + data=self._reach.reach, + toolbar=None, + display_current=False ) self.plot.draw() @@ -138,18 +138,18 @@ class FrictionsWindow(PamhyrWindow): self.plot_layout_2.addWidget(self.canvas_2) self.plot_2 = PlotStricklers( - canvas = self.canvas_2, - data = self._reach, - toolbar = None + canvas=self.canvas_2, + data=self._reach, + toolbar=None ) self.plot_2.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.find(QAction, "action_edit_stricklers").triggered.connect(self.edit_stricklers) + self.find(QAction, "action_edit_stricklers").triggered.connect( + self.edit_stricklers) table = self.find(QTableView, f"tableView") table.selectionModel()\ @@ -185,17 +185,17 @@ class FrictionsWindow(PamhyrWindow): highlight = (sec.begin_kp, sec.end_kp) self.plot = PlotKPZ( - canvas = self.canvas, - data = reach, - toolbar = None, - display_current = False + canvas=self.canvas, + data=reach, + toolbar=None, + display_current=False ) self.plot.draw(highlight=highlight) self.plot = PlotStricklers( - canvas = self.canvas_2, - data = data, - toolbar = None + canvas=self.canvas_2, + data=data, + toolbar=None ) self.plot.draw(highlight=highlight) @@ -239,14 +239,14 @@ class FrictionsWindow(PamhyrWindow): def edit_stricklers(self): strick = self.sub_win_filter_first( "Stricklers", - contain = [] + contain=[] ) if strick is None: strick = StricklersWindow( - study = self._study, - config = self.parent.conf, - parent = self + study=self._study, + config=self.parent.conf, + parent=self ) strick.show() else: diff --git a/src/View/Frictions/translate.py b/src/View/Frictions/translate.py index 1014d8c1..b3414551 100644 --- a/src/View/Frictions/translate.py +++ b/src/View/Frictions/translate.py @@ -22,6 +22,7 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class FrictionsTranslate(PamhyrTranslate): def __init__(self): super(FrictionsTranslate, self).__init__() diff --git a/src/View/Geometry/PlotAC.py b/src/View/Geometry/PlotAC.py index 974706aa..b67394c2 100644 --- a/src/View/Geometry/PlotAC.py +++ b/src/View/Geometry/PlotAC.py @@ -29,15 +29,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class PlotAC(PamhyrPlot): def __init__(self, canvas=None, trad=None, data=None, toolbar=None, plot_xy=None, parent=None): super(PlotAC, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self.plot_xy = plot_xy @@ -79,9 +80,11 @@ class PlotAC(PamhyrPlot): ) self.canvas.figure.tight_layout() - label_before_plot_selected = _translate("MainWindow_reach", "Previous cross-section") + label_before_plot_selected = _translate( + "MainWindow_reach", "Previous cross-section") label_plot_selected = _translate("MainWindow_reach", "Cross-section") - label_after_plot_selected = _translate("MainWindow_reach", "Next cross-section") + label_after_plot_selected = _translate( + "MainWindow_reach", "Next cross-section") color_before_plot_selected = "k" # 'grey' color_plot_selected = 'b' color_after_plot_selected = 'm' @@ -133,10 +136,12 @@ class PlotAC(PamhyrPlot): x_gl_complete.append(station[i]) y_gl_complete.append(elevation[i]) - color_scat_complete_gl.append(self.color_complete_gl[lcomplete.index(txt)]) + color_scat_complete_gl.append( + self.color_complete_gl[lcomplete.index(txt)]) elif txt.strip() in self.incomplete_gl: annotate = self.canvas.axes.annotate( - txt, (station[i], elevation[i]), horizontalalignment='left', + txt, (station[i], elevation[i] + ), horizontalalignment='left', verticalalignment='top', annotation_clip=True, fontsize=11, color=self.color_incomplete_gl[ lincomplete.index(txt) @@ -179,9 +184,12 @@ class PlotAC(PamhyrPlot): ) self.canvas.figure.tight_layout() - label_before_plot_selected = _translate("MainWindow_reach", "Profil précédent") - label_plot_selected = _translate("MainWindow_reach", "Profil sélectionné") - label_after_plot_selected = _translate("MainWindow_reach", "Profil suivant") + label_before_plot_selected = _translate( + "MainWindow_reach", "Profil précédent") + label_plot_selected = _translate( + "MainWindow_reach", "Profil sélectionné") + label_after_plot_selected = _translate( + "MainWindow_reach", "Profil suivant") color_before_plot_selected = "k" # 'grey' color_plot_selected = 'b' color_after_plot_selected = 'm' @@ -233,10 +241,12 @@ class PlotAC(PamhyrPlot): x_gl_complete.append(station[i]) y_gl_complete.append(elevation[i]) - color_scat_complete_gl.append(self.color_complete_gl[lcomplete.index(txt)]) + color_scat_complete_gl.append( + self.color_complete_gl[lcomplete.index(txt)]) elif txt.strip() in self.incomplete_gl: annotate = self.canvas.axes.annotate( - txt, (station[i], elevation[i]), horizontalalignment='left', + txt, (station[i], elevation[i] + ), horizontalalignment='left', verticalalignment='top', annotation_clip=True, fontsize=11, color=self.color_incomplete_gl[ lincomplete.index(txt) @@ -311,7 +321,6 @@ class PlotAC(PamhyrPlot): self.canvas.figure.canvas.draw_idle() - @timer def update(self, ind=None): if self._init == False: diff --git a/src/View/Geometry/PlotKPZ.py b/src/View/Geometry/PlotKPZ.py index f216443c..983cc798 100644 --- a/src/View/Geometry/PlotKPZ.py +++ b/src/View/Geometry/PlotKPZ.py @@ -29,15 +29,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class PlotKPZ(PamhyrPlot): def __init__(self, canvas=None, trad=None, data=None, toolbar=None, display_current=True, parent=None): super(PlotKPZ, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self.display_current = display_current @@ -75,7 +76,7 @@ class PlotKPZ(PamhyrPlot): z_max = self.data.get_z_max() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) self.line_kp_zmin_zmax = self.canvas.axes.vlines( @@ -98,7 +99,7 @@ class PlotKPZ(PamhyrPlot): ) ) - indexes_filter = lambda data: list( + def indexes_filter(data): return list( map( lambda x: x[1], filter( diff --git a/src/View/Geometry/PlotXY.py b/src/View/Geometry/PlotXY.py index ce93fa24..8c0edcbe 100644 --- a/src/View/Geometry/PlotXY.py +++ b/src/View/Geometry/PlotXY.py @@ -25,15 +25,16 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate + class PlotXY(PamhyrPlot): def __init__(self, canvas=None, trad=None, data=None, toolbar=None, display_current=True, parent=None): super(PlotXY, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self.display_current = display_current @@ -74,7 +75,7 @@ class PlotXY(PamhyrPlot): kp = self.data.get_kp() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) # Draw line for each profile @@ -85,8 +86,8 @@ class PlotXY(PamhyrPlot): markersize=3, marker='+' ) for x, y, kp in zip( - self.data.get_x(), self.data.get_y(), - kp + self.data.get_x(), self.data.get_y(), + kp ) ] diff --git a/src/View/Geometry/Profile/Plot.py b/src/View/Geometry/Profile/Plot.py index 12bb9aee..1e997685 100644 --- a/src/View/Geometry/Profile/Plot.py +++ b/src/View/Geometry/Profile/Plot.py @@ -30,15 +30,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class Plot(PamhyrPlot): def __init__(self, canvas=None, trad=None, data=None, toolbar=None, table=None, parent=None): super(Plot, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self._table = table @@ -63,7 +64,7 @@ class Plot(PamhyrPlot): y_carto = self.data.y() if (len(x_carto) < 3 or len(y_carto) < 3 or - len(x) < 3): + len(x) < 3): # Noting to do in this case return diff --git a/src/View/Geometry/Profile/Translate.py b/src/View/Geometry/Profile/Translate.py index 148426aa..27a98c1e 100644 --- a/src/View/Geometry/Profile/Translate.py +++ b/src/View/Geometry/Profile/Translate.py @@ -23,6 +23,7 @@ from View.Geometry.Translate import GeometryTranslate _translate = QCoreApplication.translate + class GeometryProfileTranslate(GeometryTranslate): def __init__(self): super(GeometryProfileTranslate, self).__init__() diff --git a/src/View/Geometry/Profile/UndoCommand.py b/src/View/Geometry/Profile/UndoCommand.py index 7f0cd138..c1122a68 100644 --- a/src/View/Geometry/Profile/UndoCommand.py +++ b/src/View/Geometry/Profile/UndoCommand.py @@ -34,6 +34,7 @@ class SetDataCommand(QUndoCommand): self._old = old_value self._new = self.type(new_value) + class SetXCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = float @@ -45,6 +46,7 @@ class SetXCommand(SetDataCommand): def redo(self): self._profile.point(self._index).x = self._new + class SetYCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = float @@ -56,6 +58,7 @@ class SetYCommand(SetDataCommand): def redo(self): self._profile.point(self._index).y = self._new + class SetZCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = float @@ -67,10 +70,12 @@ class SetZCommand(SetDataCommand): def redo(self): self._profile.point(self._index).z = self._new + class SetNameCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = str - super(SetNameCommand, self).__init__(reach, index, old_value, new_value) + super(SetNameCommand, self).__init__( + reach, index, old_value, new_value) def undo(self): self._profile.point(self._index).name = self._old @@ -96,6 +101,7 @@ class AddCommand(QUndoCommand): else: self._profile.insert_point(self._index, self._point) + class DelCommand(QUndoCommand): def __init__(self, profile, rows): QUndoCommand.__init__(self) @@ -115,6 +121,7 @@ class DelCommand(QUndoCommand): def redo(self): self._profile.delete(self._rows) + class SortCommand(QUndoCommand): def __init__(self, profile, column, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index e4112f40..dfce6d9d 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -58,11 +58,11 @@ class ProfileWindow(PamhyrWindow): name = f"{self._pamhyr_name} - {self._profile.name} {self._profile.kp}" super(ProfileWindow, self).__init__( - title = name, - study = study, - config = config, - trad = GeometryProfileTranslate(), - parent = parent + title=name, + study=study, + config=config, + trad=GeometryProfileTranslate(), + parent=parent ) self.setup_table() @@ -74,11 +74,11 @@ class ProfileWindow(PamhyrWindow): table = self.find(QTableView, "tableView") self._tablemodel = GeometryProfileTableModel( - table_view = table, - table_headers = table_headers, - editable_headers = ["name", "x", "y", "z"], - data = self._profile, - undo = self._undo_stack + table_view=table, + table_headers=table_headers, + editable_headers=["name", "x", "y", "z"], + data=self._profile, + undo=self._undo_stack ) table.setModel(self._tablemodel) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -92,17 +92,17 @@ class ProfileWindow(PamhyrWindow): self._canvas.setObjectName("canvas") self._toolbar = PamhyrPlotToolbar( self._canvas, self, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] + items=["home", "zoom", "save", "iso", "back/forward", "move"] ) self._plot_layout = self.find(QVBoxLayout, "verticalLayout") self._plot_layout.addWidget(self._toolbar) self._plot_layout.addWidget(self._canvas) self._plot = Plot( - canvas = self._canvas, - data = self._profile, - toolbar = self._toolbar, - table = self.find(QTableView, "tableView") + canvas=self._canvas, + data=self._profile, + toolbar=self._toolbar, + table=self.find(QTableView, "tableView") ) self._plot.draw() @@ -134,15 +134,15 @@ class ProfileWindow(PamhyrWindow): def index_selected_row(self): rows = self._tablemodel\ - .selectionModel()\ - .selectedRows() + .selectionModel()\ + .selectedRows() if len(rows) == 0: return 0 return self._tablemodel\ - .selectionModel()\ - .selectedRows()[0]\ - .row() + .selectionModel()\ + .selectedRows()[0]\ + .row() def add(self): table = self.find(QTableView, "tableView") @@ -213,8 +213,8 @@ class ProfileWindow(PamhyrWindow): def copy(self): rows = self._tablemodel\ - .selectionModel()\ - .selectedRows() + .selectionModel()\ + .selectedRows() table = [] table.append(["x", "y", "z", "name"]) diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index e9f6eac6..2abb2e71 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -66,10 +66,10 @@ class GeometryReachTableModel(PamhyrTableModel): if role == Qt.ForegroundRole and index.column() == 0: name = self._data.profile(index.row()).name\ - .strip()\ - .lower() + .strip()\ + .lower() if (name == "upstream" or name == "up" or - name == _translate("Geometry", "upstream")): + name == _translate("Geometry", "upstream")): return QColor("Green") elif (name == "downstream" or name == "down" or name == _translate("Geometry", "downstream")): @@ -97,7 +97,7 @@ class GeometryReachTableModel(PamhyrTableModel): SetKPCommand( self._data, index.row(), self._data.profile(index.row()).kp, - value + value ) ) except Exception as e: @@ -151,7 +151,6 @@ class GeometryReachTableModel(PamhyrTableModel): self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - def move_up(self, row, parent=QModelIndex()): if row <= 0: return @@ -199,7 +198,6 @@ class GeometryReachTableModel(PamhyrTableModel): self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - def paste(self, row, header, data): if row > self._data.number_profiles: return diff --git a/src/View/Geometry/Translate.py b/src/View/Geometry/Translate.py index c583732d..8ad7e2de 100644 --- a/src/View/Geometry/Translate.py +++ b/src/View/Geometry/Translate.py @@ -22,13 +22,16 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class GeometryTranslate(PamhyrTranslate): def __init__(self): super(GeometryTranslate, self).__init__() self._dict["open_file"] = _translate("Geometry", "Open a file") - self._dict["file_st"] = _translate("Geometry", "File mage geometry (*.ST)") - self._dict["file_m"] = _translate("Geometry", "File mage meshed geometry (*.M)") + self._dict["file_st"] = _translate( + "Geometry", "File mage geometry (*.ST)") + self._dict["file_m"] = _translate( + "Geometry", "File mage meshed geometry (*.M)") self._dict["file_all"] = _translate("Geometry", "All file (*)") self._dict["reach"] = _translate("Geometry", "reach") diff --git a/src/View/Geometry/UndoCommand.py b/src/View/Geometry/UndoCommand.py index 1df9c58c..147ce231 100644 --- a/src/View/Geometry/UndoCommand.py +++ b/src/View/Geometry/UndoCommand.py @@ -35,10 +35,12 @@ class SetDataCommand(QUndoCommand): self._old = old_value self._new = self.type(new_value) + class SetNameCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = str - super(SetNameCommand, self).__init__(reach, index, old_value, new_value) + super(SetNameCommand, self).__init__( + reach, index, old_value, new_value) def undo(self): self._reach.profile(self._index).name = self._old @@ -46,6 +48,7 @@ class SetNameCommand(SetDataCommand): def redo(self): self._reach.profile(self._index).name = self._new + class SetKPCommand(SetDataCommand): def __init__(self, reach, index, old_value, new_value): self.type = float @@ -75,6 +78,7 @@ class AddCommand(QUndoCommand): else: self._reach.insert_profile(self._index, self._profile) + class DelCommand(QUndoCommand): def __init__(self, reach, rows): QUndoCommand.__init__(self) @@ -94,6 +98,7 @@ class DelCommand(QUndoCommand): def redo(self): self._reach.delete(self._rows) + class SortCommand(QUndoCommand): def __init__(self, reach, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 516bf66f..201d6d3e 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -66,11 +66,11 @@ class GeometryWindow(PamhyrWindow): name = f"{self._pamhyr_name} - {self._reach.name}" super(GeometryWindow, self).__init__( - title = name, - study = study, - config = config, - trad = GeometryTranslate(), - parent = parent + title=name, + study=study, + config=config, + trad=GeometryTranslate(), + parent=parent ) self._tablemodel = None @@ -87,11 +87,11 @@ class GeometryWindow(PamhyrWindow): table = self.find(QTableView, "tableView") self._tablemodel = GeometryReachTableModel( - table_view = table, - table_headers = table_headers, - editable_headers = ["name", "kp"], - data = self._reach, - undo = self._undo_stack + table_view=table, + table_headers=table_headers, + editable_headers=["name", "kp"], + data=self._reach, + undo=self._undo_stack ) table.setModel(self._tablemodel) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -103,7 +103,7 @@ class GeometryWindow(PamhyrWindow): self._canvas_xy.setObjectName("canvas_xy") self._toolbar_xy = PamhyrPlotToolbar( self._canvas_xy, self, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] + items=["home", "zoom", "save", "iso", "back/forward", "move"] ) self._plot_layout_xy = self.find(QVBoxLayout, "verticalLayout") self._plot_layout_xy.addWidget(self._toolbar_xy) @@ -114,7 +114,7 @@ class GeometryWindow(PamhyrWindow): self._canvas_kpc.setObjectName("canvas_kpc") self._toolbar_kpc = PamhyrPlotToolbar( self._canvas_kpc, self, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] + items=["home", "zoom", "save", "iso", "back/forward", "move"] ) self._plot_layout_kpc = self.find(QVBoxLayout, "verticalLayout_2") self._plot_layout_kpc.addWidget(self._toolbar_kpc) @@ -125,7 +125,7 @@ class GeometryWindow(PamhyrWindow): self._canvas_ac.setObjectName("canvas_ac") self._toolbar_ac = PamhyrPlotToolbar( self._canvas_ac, self, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] + items=["home", "zoom", "save", "iso", "back/forward", "move"] ) self._plot_layout_ac = self.find(QVBoxLayout, "verticalLayout_3") self._plot_layout_ac.addWidget(self._toolbar_ac) @@ -169,19 +169,22 @@ class GeometryWindow(PamhyrWindow): self.find(QAction, action)\ .triggered.connect(actions[action]) - self.find(QSlider, "verticalSlider").valueChanged.connect(self.changed_slider_value) - self.find(QPushButton, "pushButton_up").clicked.connect(self.decrement_value_slider) - self.find(QPushButton, "pushButton_down").clicked.connect(self.increment_value_slider) + self.find(QSlider, "verticalSlider").valueChanged.connect( + self.changed_slider_value) + self.find(QPushButton, "pushButton_up").clicked.connect( + self.decrement_value_slider) + self.find(QPushButton, "pushButton_down").clicked.connect( + self.increment_value_slider) # Profile selection when line change in table self.find(QTableView, "tableView").selectionModel()\ .selectionChanged\ .connect(self.select_current_profile) - def import_from_file(self): options = QFileDialog.Options() - settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) + settings = QSettings(QSettings.IniFormat, + QSettings.UserScope, 'MyOrg', ) options |= QFileDialog.DontUseNativeDialog file_types = [ @@ -222,13 +225,13 @@ class GeometryWindow(PamhyrWindow): win = self.sub_win_filter_first( "Profile", - contain = [self._reach.name, str(profile.kp)] + contain=[self._reach.name, str(profile.kp)] ) if win is None: win = ProfileWindow( - profile = profile, - parent = self, + profile=profile, + parent=self, ) self._profile_window.append(win) win.show() @@ -250,9 +253,9 @@ class GeometryWindow(PamhyrWindow): self.tableView.model().blockSignals(True) self._plot_xy = PlotXY( - canvas = self._canvas_xy, - data = self._reach, - toolbar = self._toolbar_xy + canvas=self._canvas_xy, + data=self._reach, + toolbar=self._toolbar_xy ) self._plot_xy.draw() @@ -267,9 +270,9 @@ class GeometryWindow(PamhyrWindow): self.tableView.model().blockSignals(True) self._plot_kpc = PlotKPZ( - canvas = self._canvas_kpc, - data = self._reach, - toolbar = self._toolbar_kpc + canvas=self._canvas_kpc, + data=self._reach, + toolbar=self._toolbar_kpc ) self._plot_kpc.draw() @@ -284,10 +287,10 @@ class GeometryWindow(PamhyrWindow): self.tableView.model().blockSignals(True) self._plot_ac = PlotAC( - canvas = self._canvas_ac, - data = self._reach, - toolbar = self._toolbar_ac, - plot_xy = self._plot_xy + canvas=self._canvas_ac, + data=self._reach, + toolbar=self._toolbar_ac, + plot_xy=self._plot_xy ) self._plot_ac.draw() @@ -489,14 +492,16 @@ class GeometryWindow(PamhyrWindow): def export_to_file(self): options = QFileDialog.Options() DEFAULT_DIRECTORY = '/home/' - settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) - current_dir = settings.value('current_directory', DEFAULT_DIRECTORY, type=str) + settings = QSettings(QSettings.IniFormat, + QSettings.UserScope, 'MyOrg', ) + current_dir = settings.value( + 'current_directory', DEFAULT_DIRECTORY, type=str) options |= QFileDialog.DontUseNativeDialog filename, filters = QFileDialog.getSaveFileName( self, - filter = self._trad["file_st"] + ";; " + self._trad["file_all"], - options = options + filter=self._trad["file_st"] + ";; " + self._trad["file_all"], + options=options ) current_dir = os.path.split(filename)[0] or DEFAULT_DIRECTORY diff --git a/src/View/InitialConditions/DialogDischarge.py b/src/View/InitialConditions/DialogDischarge.py index 7be75baf..df0461bd 100644 --- a/src/View/InitialConditions/DialogDischarge.py +++ b/src/View/InitialConditions/DialogDischarge.py @@ -31,15 +31,16 @@ from PyQt5.QtWidgets import ( QDoubleSpinBox, ) + class DischargeDialog(PamhyrDialog): _pamhyr_ui = "InitialConditions_Dialog_Generator_Discharge" _pamhyr_name = "Discharge" def __init__(self, title="Discharge", parent=None): super(DischargeDialog, self).__init__( - title = self._pamhyr_name, - options = [], - parent = parent + title=self._pamhyr_name, + options=[], + parent=parent ) self.value = None diff --git a/src/View/InitialConditions/DialogHeight.py b/src/View/InitialConditions/DialogHeight.py index 8a2a3c12..0cd62c88 100644 --- a/src/View/InitialConditions/DialogHeight.py +++ b/src/View/InitialConditions/DialogHeight.py @@ -31,15 +31,16 @@ from PyQt5.QtWidgets import ( QDoubleSpinBox, ) + class HeightDialog(PamhyrDialog): _pamhyr_ui = "InitialConditions_Dialog_Generator_Height" _pamhyr_name = "Height" def __init__(self, parent=None): super(HeightDialog, self).__init__( - title = self._pamhyr_name, - options = [], - parent = parent + title=self._pamhyr_name, + options=[], + parent=parent ) self.value = None diff --git a/src/View/InitialConditions/PlotDKP.py b/src/View/InitialConditions/PlotDKP.py index d604179a..ccb774da 100644 --- a/src/View/InitialConditions/PlotDKP.py +++ b/src/View/InitialConditions/PlotDKP.py @@ -25,6 +25,7 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate + class PlotDKP(PamhyrPlot): @timer def draw(self, highlight=None): @@ -47,7 +48,7 @@ class PlotDKP(PamhyrPlot): z_min = self.data.reach.reach.get_z_min() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) self.line_kp_zmin = self.canvas.axes.plot( diff --git a/src/View/InitialConditions/PlotDischarge.py b/src/View/InitialConditions/PlotDischarge.py index 1ad2a266..d2635a6a 100644 --- a/src/View/InitialConditions/PlotDischarge.py +++ b/src/View/InitialConditions/PlotDischarge.py @@ -19,6 +19,7 @@ from tools import timer from View.Tools.PamhyrPlot import PamhyrPlot + class PlotDischarge(PamhyrPlot): @timer def draw(self, highlight=None): @@ -40,7 +41,7 @@ class PlotDischarge(PamhyrPlot): kp = self.data.reach.reach.get_kp() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) if len(self.data) != 0: diff --git a/src/View/InitialConditions/Table.py b/src/View/InitialConditions/Table.py index f7ad1020..d9546a49 100644 --- a/src/View/InitialConditions/Table.py +++ b/src/View/InitialConditions/Table.py @@ -45,6 +45,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate + class ComboBoxDelegate(QItemDelegate): def __init__(self, reach=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) diff --git a/src/View/InitialConditions/UndoCommand.py b/src/View/InitialConditions/UndoCommand.py index c96f90fa..72054009 100644 --- a/src/View/InitialConditions/UndoCommand.py +++ b/src/View/InitialConditions/UndoCommand.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from Model.InitialConditions.InitialConditions import InitialConditions from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict + class SetCommand(QUndoCommand): def __init__(self, ics, row, column, new_value): QUndoCommand.__init__(self) @@ -47,6 +48,7 @@ class SetCommand(QUndoCommand): def redo(self): self._ics.get(self._row)[self._column] = self._new + class AddCommand(QUndoCommand): def __init__(self, ics, index): QUndoCommand.__init__(self) @@ -64,6 +66,7 @@ class AddCommand(QUndoCommand): else: self._ics.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, ics, rows): QUndoCommand.__init__(self) @@ -83,6 +86,7 @@ class DelCommand(QUndoCommand): def redo(self): self._ics.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, ics, _reverse): QUndoCommand.__init__(self) @@ -168,6 +172,7 @@ class DuplicateCommand(QUndoCommand): for ic in self._ics: self._ics.insert(self._rows[0], ic) + class GenerateCommand(QUndoCommand): def __init__(self, ics, generator, param): QUndoCommand.__init__(self) diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 90cf8e08..5f270599 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -60,6 +60,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class InitialConditionsWindow(PamhyrWindow): _pamhyr_ui = "InitialConditions" _pamhyr_name = "Initial condition" @@ -77,11 +78,11 @@ class InitialConditionsWindow(PamhyrWindow): ) super(InitialConditionsWindow, self).__init__( - title = name, - study = study, - config = config, - trad = ICTranslate(), - parent = parent + title=name, + study=study, + config=config, + trad=ICTranslate(), + parent=parent ) self._ics = study.river.initial_conditions.get(self._reach) @@ -95,19 +96,19 @@ class InitialConditionsWindow(PamhyrWindow): def setup_table(self): table = self.find(QTableView, f"tableView") self._delegate_kp = ComboBoxDelegate( - reach = self._reach, - parent = self + reach=self._reach, + parent=self ) self._table = InitialConditionTableModel( - reach = self._reach, - table_view = table, - table_headers = self._trad.get_dict("table_headers"), - editable_headers = ["kp", "discharge", "elevation", "height"], - delegates = {"kp": self._delegate_kp}, - data = self._study, - undo = self._undo_stack, - trad = self._trad + reach=self._reach, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["kp", "discharge", "elevation", "height"], + delegates={"kp": self._delegate_kp}, + data=self._study, + undo=self._undo_stack, + trad=self._trad ) table.setModel(self._table) @@ -126,11 +127,11 @@ class InitialConditionsWindow(PamhyrWindow): self.plot_layout_1.addWidget(self.canvas_1) self.plot_1 = PlotDKP( - canvas = self.canvas_1, - data = self._ics, - trad = self._trad, - toolbar = self.toolbar_1, - parent = self + canvas=self.canvas_1, + data=self._ics, + trad=self._trad, + toolbar=self.toolbar_1, + parent=self ) self.plot_1.draw() @@ -144,11 +145,11 @@ class InitialConditionsWindow(PamhyrWindow): self.plot_layout_2.addWidget(self.canvas_2) self.plot_2 = PlotDischarge( - canvas = self.canvas_2, - data = self._ics, - trad = self._trad, - toolbar = self.toolbar_2, - parent = self + canvas=self.canvas_2, + data=self._ics, + trad=self._trad, + toolbar=self.toolbar_2, + parent=self ) self.plot_2.draw() diff --git a/src/View/InitialConditions/translate.py b/src/View/InitialConditions/translate.py index f65306cb..00cdf362 100644 --- a/src/View/InitialConditions/translate.py +++ b/src/View/InitialConditions/translate.py @@ -22,12 +22,15 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class ICTranslate(PamhyrTranslate): def __init__(self): super(ICTranslate, self).__init__() - self._dict["elevation"] = _translate("InitialCondition", "Elevation (m)") - self._dict["discharge"] = _translate("InitialCondition", "Discharge (m³/s)") + self._dict["elevation"] = _translate( + "InitialCondition", "Elevation (m)") + self._dict["discharge"] = _translate( + "InitialCondition", "Discharge (m³/s)") self._dict["kp"] = _translate("InitialCondition", "KP (m)") self._sub_dict["table_headers"] = { diff --git a/src/View/LateralContribution/Edit/Plot.py b/src/View/LateralContribution/Edit/Plot.py index ea1c0454..336b154b 100644 --- a/src/View/LateralContribution/Edit/Plot.py +++ b/src/View/LateralContribution/Edit/Plot.py @@ -29,16 +29,17 @@ from View.LateralContribution.Edit.translate import * _translate = QCoreApplication.translate + class Plot(PamhyrPlot): - def __init__(self, mode = "time", data=None, + def __init__(self, mode="time", data=None, canvas=None, trad=None, toolbar=None, parent=None): super(Plot, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self._table_headers = self._trad.get_dict("table_headers") @@ -68,8 +69,8 @@ class Plot(PamhyrPlot): map( lambda v: str( datetime.fromtimestamp(v[0]) - t0 - ).split(",")[0]\ - .replace("days", _translate("LateralContribution", "days"))\ + ).split(",")[0] + .replace("days", _translate("LateralContribution", "days")) .replace("day", _translate("LateralContribution", "day")), fx ) @@ -84,7 +85,6 @@ class Plot(PamhyrPlot): self.canvas.axes.set_xticks(ticks=xx, labels=xt, rotation=45) - @timer def draw(self): self.canvas.axes.cla() @@ -118,7 +118,7 @@ class Plot(PamhyrPlot): self.canvas.axes.autoscale_view(True, True, True) self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() - #self.toolbar.update() + # self.toolbar.update() self._init = True diff --git a/src/View/LateralContribution/Edit/UndoCommand.py b/src/View/LateralContribution/Edit/UndoCommand.py index 715683c9..780ea8ac 100644 --- a/src/View/LateralContribution/Edit/UndoCommand.py +++ b/src/View/LateralContribution/Edit/UndoCommand.py @@ -25,6 +25,7 @@ from PyQt5.QtWidgets import ( from Model.LateralContribution.LateralContribution import LateralContribution + class SetDataCommand(QUndoCommand): def __init__(self, data, index, column, new_value): QUndoCommand.__init__(self) @@ -42,6 +43,7 @@ class SetDataCommand(QUndoCommand): def redo(self): self._data._set_i_c_v(self._index, self._column, self._new) + class AddCommand(QUndoCommand): def __init__(self, data, index): QUndoCommand.__init__(self) @@ -59,6 +61,7 @@ class AddCommand(QUndoCommand): else: self._data.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, data, rows): QUndoCommand.__init__(self) @@ -78,6 +81,7 @@ class DelCommand(QUndoCommand): def redo(self): self._data.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, data, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 55f1307f..c5d12a38 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -45,6 +45,7 @@ from View.LateralContribution.Edit.Plot import Plot _translate = QCoreApplication.translate + class EditLateralContributionWindow(PamhyrWindow): _pamhyr_ui = "EditLateralContribution" _pamhyr_name = "Edit lateral contribution" @@ -67,11 +68,11 @@ class EditLateralContributionWindow(PamhyrWindow): ) super(EditLateralContributionWindow, self).__init__( - title = name, - study = study, - config = config, - trad = trad, - parent = parent + title=name, + study=study, + config=config, + trad=trad, + parent=parent ) self.setup_table() @@ -80,9 +81,9 @@ class EditLateralContributionWindow(PamhyrWindow): def setup_table(self): self._delegate_time = PamhyrExTimeDelegate( - data = self._data, - mode = self._study.time_system, - parent = self + data=self._data, + mode=self._study.time_system, + parent=self ) headers = {} @@ -92,16 +93,16 @@ class EditLateralContributionWindow(PamhyrWindow): table = self.find(QTableView, "tableView") self._table = TableModel( - table_view = table, - table_headers = headers, - editable_headers = self._data.header, - delegates = { + table_view=table, + table_headers=headers, + editable_headers=self._data.header, + delegates={ "time": self._delegate_time, }, - data = self._data, - undo = self._undo_stack, - trad = self._trad, - opt_data = self._study.time_system + data=self._data, + undo=self._undo_stack, + trad=self._trad, + opt_data=self._study.time_system ) table.setModel(self._table) @@ -119,12 +120,12 @@ class EditLateralContributionWindow(PamhyrWindow): self.verticalLayout.addWidget(self.canvas) self.plot = Plot( - canvas = self.canvas, - data = self._data, - mode = self._study.time_system, - toolbar = self.toolbar, - trad = self._trad, - parent = self + canvas=self.canvas, + data=self._data, + mode=self._study.time_system, + toolbar=self.toolbar, + trad=self._trad, + parent=self ) self.plot.draw() @@ -156,7 +157,6 @@ class EditLateralContributionWindow(PamhyrWindow): ) ) - def add(self): rows = self.index_selected_rows() if len(self._data) == 0 or len(rows) == 0: diff --git a/src/View/LateralContribution/Edit/translate.py b/src/View/LateralContribution/Edit/translate.py index 247cdc4e..9fc86e4d 100644 --- a/src/View/LateralContribution/Edit/translate.py +++ b/src/View/LateralContribution/Edit/translate.py @@ -23,6 +23,7 @@ from View.LateralContribution.translate import LCTranslate _translate = QCoreApplication.translate + class LCETranslate(LCTranslate): def __init__(self): super(LCETranslate, self).__init__() diff --git a/src/View/LateralContribution/Table.py b/src/View/LateralContribution/Table.py index 3c5f5cb7..95d09c9c 100644 --- a/src/View/LateralContribution/Table.py +++ b/src/View/LateralContribution/Table.py @@ -52,8 +52,9 @@ logger = logging.getLogger() _translate = QCoreApplication.translate + class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, mode="type", tab="", trad = None, parent=None): + def __init__(self, data=None, mode="type", tab="", trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._data = data @@ -154,7 +155,8 @@ class TableModel(PamhyrTableModel): ) ) elif self._headers[column] == "type": - key = next(k for k, v in self._long_types.items() if v == value) + key = next(k for k, v in self._long_types.items() + if v == value) self._undo.push( SetTypeCommand( self._lst, self._tab, row, LC_types[key] diff --git a/src/View/LateralContribution/UndoCommand.py b/src/View/LateralContribution/UndoCommand.py index 3d858436..198fd959 100644 --- a/src/View/LateralContribution/UndoCommand.py +++ b/src/View/LateralContribution/UndoCommand.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from Model.LateralContribution.LateralContribution import LateralContribution from Model.LateralContribution.LateralContributionList import LateralContributionList + class SetNameCommand(QUndoCommand): def __init__(self, lcs, tab, index, new_value): QUndoCommand.__init__(self) @@ -42,6 +43,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._lcs.get(self._tab, self._index).name = self._new + class SetBeginCommand(QUndoCommand): def __init__(self, lcs, tab, index, new_value): QUndoCommand.__init__(self) @@ -58,6 +60,7 @@ class SetBeginCommand(QUndoCommand): def redo(self): self._lcs.get(self._tab, self._index).begin_kp = float(self._new) + class SetEndCommand(QUndoCommand): def __init__(self, lcs, tab, index, new_value): QUndoCommand.__init__(self) @@ -91,6 +94,7 @@ class SetEdgeCommand(QUndoCommand): def redo(self): self._lcs.get(self._tab, self._index).edge = self._new + class SetTypeCommand(QUndoCommand): def __init__(self, lcs, tab, index, _type): QUndoCommand.__init__(self) @@ -109,6 +113,7 @@ class SetTypeCommand(QUndoCommand): def redo(self): self._lcs.set(self._tab, self._index, self._new) + class AddCommand(QUndoCommand): def __init__(self, lcs, tab, index): QUndoCommand.__init__(self) @@ -127,6 +132,7 @@ class AddCommand(QUndoCommand): else: self._lcs.insert(self._tab, self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, lcs, tab, rows): QUndoCommand.__init__(self) @@ -147,6 +153,7 @@ class DelCommand(QUndoCommand): def redo(self): self._lcs.delete_i(self._tab, self._rows) + class SortCommand(QUndoCommand): def __init__(self, lcs, tab, _reverse): QUndoCommand.__init__(self) diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index 467f2a4b..72ca343d 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -62,6 +62,7 @@ from View.LateralContribution.Edit.Window import EditLateralContributionWindow logger = logging.getLogger() + class LateralContributionWindow(PamhyrWindow): _pamhyr_ui = "LateralContributions" _pamhyr_name = "Lateral contribution" @@ -70,10 +71,10 @@ class LateralContributionWindow(PamhyrWindow): name = self._pamhyr_name + " - " + study.name super(LateralContributionWindow, self).__init__( - title = name, - study = study, - trad = LCTranslate(), - config = config, + title=name, + study=study, + trad=LCTranslate(), + config=config, parent=parent ) @@ -88,33 +89,33 @@ class LateralContributionWindow(PamhyrWindow): for t in ["liquid", "solid", "suspenssion"]: self._delegate_type = ComboBoxDelegate( - data = self._study.river, - mode = "type", - tab = t, - trad = self._trad, + data=self._study.river, + mode="type", + tab=t, + trad=self._trad, parent=self ) self._delegate_edge = ComboBoxDelegate( - data = self._study.river, - mode = "edge", - tab = t, - trad = self._trad, + data=self._study.river, + mode="edge", + tab=t, + trad=self._trad, parent=self ) table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( - table_view = table, - table_headers = self._trad.get_dict("table_headers"), - editable_headers = True, - delegates = { + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=True, + delegates={ "type": self._delegate_type, "edge": self._delegate_edge, }, - data = self._study.river, - undo = self._undo_stack, - trad = self._trad, - opt_data = t, + data=self._study.river, + undo=self._undo_stack, + trad=self._trad, + opt_data=t, ) table.setModel(self._table[t]) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -128,9 +129,9 @@ class LateralContributionWindow(PamhyrWindow): self.plot_layout.addWidget(self.canvas) self.plot = PlotXY( - canvas = self.canvas, - data = None, - toolbar = None, + canvas=self.canvas, + data=None, + toolbar=None, ) def setup_connections(self): @@ -193,9 +194,9 @@ class LateralContributionWindow(PamhyrWindow): highlight = (lc.begin_kp, lc.end_kp) self.plot = PlotXY( - canvas = self.canvas, - data = data, - toolbar = None, + canvas=self.canvas, + data=data, + toolbar=None, ) self.plot.draw(highlight=highlight) @@ -259,7 +260,7 @@ class LateralContributionWindow(PamhyrWindow): for row in rows: win = self.sub_win_filter_first( "Edit lateral contribution", - contain = [f"({self._lcs.get(tab, row).id})"] + contain=[f"({self._lcs.get(tab, row).id})"] ) if win is None: diff --git a/src/View/LateralContribution/translate.py b/src/View/LateralContribution/translate.py index aaf44798..f005a063 100644 --- a/src/View/LateralContribution/translate.py +++ b/src/View/LateralContribution/translate.py @@ -33,6 +33,7 @@ LC_types = { "EV": Evaporation, } + class LCTranslate(PamhyrTranslate): def __init__(self): super(LCTranslate, self).__init__() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index cf7e08a7..18c4837e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -16,7 +16,8 @@ # -*- coding: utf-8 -*- -import os, sys +import os +import sys import logging import subprocess from queue import Queue @@ -107,6 +108,7 @@ action = ( no_model_action + model_action + define_model_action ) + class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def __init__(self, conf=None): super(ApplicationWindow, self).__init__() @@ -132,10 +134,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.setup_sc() self.setup_connection() self.default_style() - self.setup_debug_mode(init = True) + self.setup_debug_mode(init=True) self.trans = QTranslator(self) - #self.ui.retranslateUi() + # self.ui.retranslateUi() if self.conf.last_study != "" and not self.conf.close_correctly: self.dialog_reopen_study() @@ -153,7 +155,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): title += "Pamhyr2" self.setWindowTitle(title) - def enable_actions(self, action:str, enable:bool): + def enable_actions(self, action: str, enable: bool): """Enable of disable an action componant Args: @@ -196,7 +198,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, "action_menu_close": self.close_model, "action_menu_results_last": self.open_last_results, - ## Help + # Help "action_menu_pamhyr_users_pdf": lambda: self.open_doc_user(ext="pdf"), "action_menu_pamhyr_developers_pdf": lambda: self.open_doc_dev(ext="pdf"), "action_menu_pamhyr_users_html": lambda: self.open_doc_user(ext="html"), @@ -209,7 +211,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_toolBar_save": self.save_study, "action_toolBar_close": self.close_model, "action_toolBar_run_solver": self.run_solver, - ## Current actions + # Current actions "action_toolBar_network": self.open_network, "action_toolBar_geometry": self.open_geometry, "action_toolBar_mesh": lambda: self.open_dummy("Mesh"), @@ -255,7 +257,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def closeEvent(self, event): if not self._close_question: if self._study is not None and not self._study.is_saved: - if self.dialog_close(cancel = False): + if self.dialog_close(cancel=False): # PAMHYR is close correctly (no crash) self.conf.set_close_correctly() super(ApplicationWindow, self).closeEvent(event) @@ -272,7 +274,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # Maximise window self.showMaximized() - def set_debug_lvl(self, debug = True): + def set_debug_lvl(self, debug=True): if debug: logger.setLevel(logging.DEBUG) logger.info("Set logging level to DEBUG") @@ -280,7 +282,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): logger.setLevel(logging.INFO) logger.info("Set logging level to INFO") - def setup_debug_mode(self, init = False): + def setup_debug_mode(self, init=False): menu = self.findChild(QMenu, "menu_help") self.set_title() @@ -290,22 +292,23 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.debug_action.triggered.connect(self.open_debug) self.debug_sqlite_action = QAction("Debug SQLite", self) - self.debug_sqlite_action.setStatusTip("Open SQLite debuging tool (sqlitebrowser)") + self.debug_sqlite_action.setStatusTip( + "Open SQLite debuging tool (sqlitebrowser)") self.debug_sqlite_action.triggered.connect(self.open_sqlite) if self.conf.debug: menu.addAction(self.debug_action) menu.addAction(self.debug_sqlite_action) - self.set_debug_lvl(debug = True) + self.set_debug_lvl(debug=True) else: if self.conf.debug: menu.addAction(self.debug_action) menu.addAction(self.debug_sqlite_action) - self.set_debug_lvl(debug = True) + self.set_debug_lvl(debug=True) else: menu.removeAction(self.debug_action) menu.removeAction(self.debug_sqlite_action) - self.set_debug_lvl(debug = False) + self.set_debug_lvl(debug=False) ######### # MODEL # @@ -430,7 +433,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): dlg.setWindowTitle("Last open study") dlg.setText("Do you want to open again the last open study?") - opt = QMessageBox.Cancel | QMessageBox.Ok #| QMessageBox.Open + opt = QMessageBox.Cancel | QMessageBox.Ok # | QMessageBox.Open dlg.setStandardButtons(opt) dlg.setIcon(QMessageBox.Question) @@ -446,7 +449,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): elif res == QMessageBox.Cancel: return False - def dialog_close(self, cancel = True): + def dialog_close(self, cancel=True): dlg = QMessageBox(self) dlg.setWindowTitle("Close PAMHYR without saving study") @@ -504,7 +507,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.FileMode.ExistingFile) dialog.setDefaultSuffix(".pamhyr") - #dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden) + # dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden) dialog.setNameFilters(['PamHyr (*.pamhyr)']) dialog.setDirectory(os.path.dirname(self.conf.last_study)) @@ -554,11 +557,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if (self._study is not None and self._study.river.has_current_reach()): geometry = self.sub_win_filter_first( "Geometry", - contain = [self._study.river.current_reach().name] + contain=[self._study.river.current_reach().name] ) if geometry is None: - geometry = GeometryWindow(study=self._study, config=self.conf, parent=self) + geometry = GeometryWindow( + study=self._study, config=self.conf, parent=self) geometry.show() else: geometry.activateWindow() @@ -568,11 +572,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_boundary_cond(self): bound = self.sub_win_filter_first( "Boundary conditions", - contain = [] + contain=[] ) if bound is None: - bound = BoundaryConditionWindow(study = self._study, parent = self) + bound = BoundaryConditionWindow(study=self._study, parent=self) bound.show() else: bound.activateWindow() @@ -580,11 +584,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_lateral_contrib(self): lateral = self.sub_win_filter_first( "Lateral contribution", - contain = [] + contain=[] ) if lateral is None: - lateral = LateralContributionWindow(study = self._study, parent = self) + lateral = LateralContributionWindow(study=self._study, parent=self) lateral.show() else: lateral.activateWindow() @@ -592,14 +596,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_stricklers(self): strick = self.sub_win_filter_first( "Stricklers", - contain = [] + contain=[] ) if strick is None: strick = StricklersWindow( - study = self._study, - config = self.conf, - parent = self + study=self._study, + config=self.conf, + parent=self ) strick.show() else: @@ -607,17 +611,17 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_frictions(self): if (self._study is not None and - self._study.river.has_current_reach()): + self._study.river.has_current_reach()): frictions = self.sub_win_filter_first( "Frictions", - contain = [self._study.river.current_reach().name] + contain=[self._study.river.current_reach().name] ) if frictions is None: frictions = FrictionsWindow( - study = self._study, - parent = self + study=self._study, + parent=self ) frictions.show() else: @@ -629,14 +633,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self._study.river.has_current_reach(): initial = self.sub_win_filter_first( "Initial condition", - contain = [self._study.river.current_reach().name] + contain=[self._study.river.current_reach().name] ) if initial is None: initial = InitialConditionsWindow( - study = self._study, - config = self.conf, - parent = self + study=self._study, + config=self.conf, + parent=self ) initial.show() else: @@ -647,13 +651,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_solver_parameters(self): params = self.sub_win_filter_first( "Solver parameters", - contain = [] + contain=[] ) if params is None: params = SolverParametersWindow( - study = self._study, - parent = self + study=self._study, + parent=self ) params.show() else: @@ -661,15 +665,15 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_sediment_layers(self): sl = SedimentLayersWindow( - study = self._study, - parent = self + study=self._study, + parent=self ) sl.show() def open_reach_sediment_layers(self): sl = ReachSedimentLayersWindow( - study = self._study, - parent = self + study=self._study, + parent=self ) sl.show() @@ -678,30 +682,30 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): return run = SelectSolverWindow( - study = self._study, - config = self.conf, - parent = self + study=self._study, + config=self.conf, + parent=self ) if run.exec(): solver = run.solver check = CheckListWindow( - study = self._study, - config = self.conf, - solver = solver, - parent = self + study=self._study, + config=self.conf, + solver=solver, + parent=self ) check.show() def solver_log(self, solver): sol = SolverLogWindow( - study = self._study, - config = self.conf, - solver = solver, - parent = self + study=self._study, + config=self.conf, + solver=solver, + parent=self ) sol.show() - def open_solver_results(self, solver, results = None): + def open_solver_results(self, solver, results=None): # If no specific results, get last results if results is None: results = self._last_results @@ -713,15 +717,15 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # Windows already opened res = self.sub_win_filter_first( "Results", - contain = [solver.name, results.date] + contain=[solver.name, results.date] ) if res is None: res = ResultsWindow( - study = self._study, - solver = solver, - results = results, - parent = self + study=self._study, + solver=solver, + results=results, + parent=self ) res.show() else: @@ -763,8 +767,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): QDesktopServices.openUrl(url) else: doc = DocWindow( - filename = filename, - parent = self + filename=filename, + parent=self ) doc.show() @@ -783,9 +787,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_debug(self): repl = ReplWindow( - study = self._study, - config = self.conf, - parent = self + study=self._study, + config=self.conf, + parent=self ) repl.show() diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index f7b5acd0..5253c37f 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -115,6 +115,7 @@ class NodeItem(QGraphicsItem): if not self.graph._only_display: super(NodeItem, self).mouseMoveEvent(event) + class EdgeItem(QGraphicsItem): Type = QGraphicsItem.UserType + 2 @@ -216,6 +217,7 @@ class EdgeItem(QGraphicsItem): painter.drawPolygon(poly) painter.fillPath(path, brush) + class NodeText(QGraphicsTextItem): def __init__(self, node_item): super(NodeText, self).__init__() @@ -228,9 +230,9 @@ class NodeText(QGraphicsTextItem): def set_custom_pos(self, pos): x = pos.x() - y = pos.y() - 42 # Dont panic! The answer is 42 + y = pos.y() - 42 # Dont panic! The answer is 42 - self.setPos(QPointF(x,y)) + self.setPos(QPointF(x, y)) @timer def paint(self, painter, option, widget): @@ -248,6 +250,7 @@ class NodeText(QGraphicsTextItem): # Update the node text self.setPlainText(self.item.node.name) + class NewEdgeLine(QGraphicsItem): def __init__(self, src, dest): super(NewEdgeLine, self).__init__() @@ -278,13 +281,14 @@ class NewEdgeLine(QGraphicsItem): Qt.RoundJoin)) painter.drawLine(line) + class GraphWidget(QGraphicsView): changeEdge = pyqtSignal(object) changeNode = pyqtSignal(object) def __init__(self, graph, parent=None, min_size=(400, 400), max_size=None, - size=None, only_display=False, undo = None): + size=None, only_display=False, undo=None): super(GraphWidget, self).__init__(parent=parent) self.timerId = 0 @@ -522,7 +526,6 @@ class GraphWidget(QGraphicsView): self.changeNode.emit(self.sender()) self.changeEdge.emit(self.sender()) - def update_edges(self, node): """Update display of all edges linked with the node @@ -791,7 +794,7 @@ class GraphWidget(QGraphicsView): if self._state == "move": # Move on scene if (not self.selected_item() and - event.buttons() & Qt.LeftButton): + event.buttons() & Qt.LeftButton): old_p = self.mapToScene(self.m_origin_x, self.m_origin_y) new_p = self.mapToScene(event.pos()) translation = new_p - old_p @@ -805,7 +808,6 @@ class GraphWidget(QGraphicsView): self.update() super(GraphWidget, self).mouseMoveEvent(event) - # Contextual menu def _menu_default(self, event, pos, items, menu): @@ -835,7 +837,8 @@ class GraphWidget(QGraphicsView): enable = menu.addAction(_translate("Network", "Enable the reach")) is_enable = False - reverse = menu.addAction(_translate("Network", "Reverse the reach orientation")) + reverse = menu.addAction(_translate( + "Network", "Reverse the reach orientation")) action = menu.exec_(self.mapToGlobal(event.pos())) diff --git a/src/View/Network/Table.py b/src/View/Network/Table.py index da74d813..8a94c8b8 100644 --- a/src/View/Network/Table.py +++ b/src/View/Network/Table.py @@ -69,6 +69,7 @@ class ComboBoxDelegate(QItemDelegate): def currentItemChanged(self): self.commitData.emit(self.sender()) + class NodeTableModel(PamhyrTableModel): def _setup_lst(self): self._lst = self._data.nodes() @@ -120,6 +121,7 @@ class NodeTableModel(PamhyrTableModel): self._lst = self._data.nodes() self.layoutChanged.emit() + class EdgeTableModel(PamhyrTableModel): def _setup_lst(self): self._lst = self._data.edges() @@ -140,7 +142,7 @@ class EdgeTableModel(PamhyrTableModel): try: if (self._headers[index.column()] == "node1" or - self._headers[index.column()] == "node2"): + self._headers[index.column()] == "node2"): node = self.graph.node(value) self._undo.push( SetNodeCommand( diff --git a/src/View/Network/UndoCommand.py b/src/View/Network/UndoCommand.py index c9cce895..1c1d9da1 100644 --- a/src/View/Network/UndoCommand.py +++ b/src/View/Network/UndoCommand.py @@ -31,6 +31,7 @@ from Model.Network.Node import Node from Model.Network.Edge import Edge from Model.Network.Graph import Graph + class AddNodeCommand(QUndoCommand): def __init__(self, graph, node): QUndoCommand.__init__(self) @@ -44,6 +45,7 @@ class AddNodeCommand(QUndoCommand): def redo(self): self._graph.insert_node(self._node) + class SetNodePosCommand(QUndoCommand): def __init__(self, node, new, old): QUndoCommand.__init__(self) @@ -58,6 +60,7 @@ class SetNodePosCommand(QUndoCommand): def redo(self): self._node.node.setPos(self._new[0], self._new[1]) + class DelNodeCommand(QUndoCommand): def __init__(self, graph, node): QUndoCommand.__init__(self) @@ -86,6 +89,7 @@ class DelNodeCommand(QUndoCommand): self._graph.remove_node(self._node.name) + class AddEdgeCommand(QUndoCommand): def __init__(self, graph, edge): QUndoCommand.__init__(self) @@ -99,6 +103,7 @@ class AddEdgeCommand(QUndoCommand): def redo(self): self._graph.insert_edge(self._edge) + class DelEdgeCommand(QUndoCommand): def __init__(self, graph, edge): QUndoCommand.__init__(self) @@ -144,6 +149,7 @@ class SetNodeCommand(QUndoCommand): def redo(self): self._el[self._column] = self._new + class EnableEdgeCommand(QUndoCommand): def __init__(self, edge, enable): QUndoCommand.__init__(self) @@ -153,10 +159,11 @@ class EnableEdgeCommand(QUndoCommand): self._enable = enable def undo(self): - self._edge.enable(enable = self._old) + self._edge.enable(enable=self._old) def redo(self): - self._edge.enable(enable = self._enable) + self._edge.enable(enable=self._enable) + class ReverseEdgeCommand(QUndoCommand): def __init__(self, edge): diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index f0458596..1f8bb811 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -57,11 +57,11 @@ class NetworkWindow(PamhyrWindow): def __init__(self, study=None, config=None, parent=None): super(NetworkWindow, self).__init__( - title = self._pamhyr_name + " - " + study.name, - study = study, - config = config, - trad = NetworkTranslate(), - options = ['undo'], + title=self._pamhyr_name + " - " + study.name, + study=study, + config=config, + trad=NetworkTranslate(), + options=['undo'], parent=parent, ) @@ -77,11 +77,11 @@ class NetworkWindow(PamhyrWindow): # Nodes table table = self.find(QTableView, "tableView_nodes") self._nodes_model = NodeTableModel( - table_view = table, - table_headers = self._table_headers_node, - editable_headers = ["name"], - data = self._graph, - undo = self._undo_stack, + table_view=table, + table_headers=self._table_headers_node, + editable_headers=["name"], + data=self._graph, + undo=self._undo_stack, ) table.setModel(self._nodes_model) @@ -91,30 +91,30 @@ class NetworkWindow(PamhyrWindow): # Edges table table = self.find(QTableView, "tableView_reachs") self.delegate_combobox = ComboBoxDelegate( - graph = self._graph, - parent = self, + graph=self._graph, + parent=self, ) self._reachs_model = EdgeTableModel( - table_view = table, - table_headers = self._table_headers_edge, - editable_headers = ["name", "node1", "node2"], - delegates = { + table_view=table, + table_headers=self._table_headers_edge, + editable_headers=["name", "node1", "node2"], + delegates={ "node1": self.delegate_combobox, "node2": self.delegate_combobox, }, - data = self._graph, - undo = self._undo_stack, + data=self._graph, + undo=self._undo_stack, ) table.setModel(self._reachs_model) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - #table.resizeColumnsToContents() + # table.resizeColumnsToContents() def setup_graph(self): self._graph_widget = GraphWidget( self._graph, parent=self, - undo = self._undo_stack + undo=self._undo_stack ) self._graph_layout = self.find(QHBoxLayout, "horizontalLayout_graph") self._graph_layout.addWidget(self._graph_widget) @@ -122,7 +122,8 @@ class NetworkWindow(PamhyrWindow): def setup_connections(self): self._nodes_model.dataChanged.connect(self._reachs_model.update) self._nodes_model.dataChanged.connect(self._graph_widget.rename_nodes) - self._reachs_model.dataChanged.connect(self._graph_widget.display_update) + self._reachs_model.dataChanged.connect( + self._graph_widget.display_update) self._reachs_model.dataChanged.connect(self._nodes_model.update) self._graph_widget.changeEdge.connect(self._reachs_model.update) self._graph_widget.changeNode.connect(self._nodes_model.update) diff --git a/src/View/Network/translate.py b/src/View/Network/translate.py index 44e97cf5..ac8431ae 100644 --- a/src/View/Network/translate.py +++ b/src/View/Network/translate.py @@ -22,6 +22,7 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class NetworkTranslate(PamhyrTranslate): def __init__(self): super(NetworkTranslate, self).__init__() diff --git a/src/View/Results/PlotAC.py b/src/View/Results/PlotAC.py index ed3d99ad..075e879f 100644 --- a/src/View/Results/PlotAC.py +++ b/src/View/Results/PlotAC.py @@ -25,16 +25,17 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate + class PlotAC(PamhyrPlot): def __init__(self, canvas=None, trad=None, toolbar=None, results=None, reach_id=0, profile_id=0, parent=None): super(PlotAC, self).__init__( - canvas = canvas, - trad = trad, - data = results, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=results, + toolbar=toolbar, + parent=parent ) self._current_timestamp = max(results.get("timestamps")) @@ -68,7 +69,7 @@ class PlotAC(PamhyrPlot): z = profile.geometry.z() self.canvas.axes.set_xlim( - left = min(x), right = max(x) + left=min(x), right=max(x) ) self.line_kp, = self.canvas.axes.plot( diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py index 99a59f88..dd5a332b 100644 --- a/src/View/Results/PlotH.py +++ b/src/View/Results/PlotH.py @@ -32,16 +32,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class PlotH(PamhyrPlot): def __init__(self, canvas=None, trad=None, toolbar=None, results=None, reach_id=0, profile_id=0, parent=None): super(PlotH, self).__init__( - canvas = canvas, - trad = trad, - data = results, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=results, + toolbar=toolbar, + parent=parent ) self._mode = "time" @@ -87,7 +88,7 @@ class PlotH(PamhyrPlot): ts.sort() self.canvas.axes.set_xlim( - left = min(ts), right = max(ts) + left=min(ts), right=max(ts) ) # Draw discharge for each timestamp @@ -103,7 +104,7 @@ class PlotH(PamhyrPlot): return self.canvas.axes.set_ylim( - [min(min(y),0), max(y) + 10] + [min(min(y), 0), max(y) + 10] ) self._line = [ @@ -136,8 +137,8 @@ class PlotH(PamhyrPlot): lambda v: ( str( datetime.fromtimestamp(v) - t0 - ).split(",")[0]\ - .replace("days", _translate("Results", "days"))\ + ).split(",")[0] + .replace("days", _translate("Results", "days")) .replace("day", _translate("Results", "day")) ), fx diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py index 61de649d..3dd789a2 100644 --- a/src/View/Results/PlotKPC.py +++ b/src/View/Results/PlotKPC.py @@ -25,16 +25,17 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate + class PlotKPC(PamhyrPlot): def __init__(self, canvas=None, trad=None, toolbar=None, results=None, reach_id=0, profile_id=0, parent=None): super(PlotKPC, self).__init__( - canvas = canvas, - trad = trad, - data = results, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=results, + toolbar=toolbar, + parent=parent ) self._current_timestamp = max(results.get("timestamps")) @@ -68,7 +69,7 @@ class PlotKPC(PamhyrPlot): z_min = reach.geometry.get_z_min() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) self.line_kp_zmin = self.canvas.axes.plot( diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py index 3163931b..406863e2 100644 --- a/src/View/Results/PlotSedProfile.py +++ b/src/View/Results/PlotSedProfile.py @@ -15,16 +15,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class PlotSedProfile(PamhyrPlot): def __init__(self, canvas=None, trad=None, toolbar=None, results=None, reach_id=0, profile_id=0, parent=None): super(PlotSedProfile, self).__init__( - canvas = canvas, - trad = trad, - data = results, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=results, + toolbar=toolbar, + parent=parent ) self._results = results @@ -59,8 +60,8 @@ class PlotSedProfile(PamhyrPlot): cur_0 = [] for p in range(profile.geometry.number_points): - cur.append(profile_sl) - cur_0.append(profile_sl_0) + cur.append(profile_sl) + cur_0.append(profile_sl_0) sl.append(cur) sl_0.append(cur_0) @@ -106,7 +107,6 @@ class PlotSedProfile(PamhyrPlot): return list(reversed(z_sl)) - @timer def draw(self): self.canvas.axes.cla() @@ -133,7 +133,7 @@ class PlotSedProfile(PamhyrPlot): z = profile.geometry.z() self.canvas.axes.set_xlim( - left = min(x), right = max(x) + left=min(x), right=max(x) ) z_sl = self.get_zsl(profile) diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py index c63c80ec..f9e774d8 100644 --- a/src/View/Results/PlotSedReach.py +++ b/src/View/Results/PlotSedReach.py @@ -15,16 +15,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class PlotSedReach(PamhyrPlot): def __init__(self, canvas=None, trad=None, toolbar=None, results=None, reach_id=0, profile_id=0, parent=None): super(PlotSedReach, self).__init__( - canvas = canvas, - trad = trad, - data = results, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=results, + toolbar=toolbar, + parent=parent ) self._results = results diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index 42716776..5a513322 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -31,16 +31,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class PlotXY(PamhyrPlot): def __init__(self, canvas=None, trad=None, toolbar=None, results=None, reach_id=0, profile_id=0, display_current=True, parent=None): super(PlotXY, self).__init__( - canvas = canvas, - trad = trad, - data = results, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=results, + toolbar=toolbar, + parent=parent ) self.display_current = display_current @@ -87,7 +88,7 @@ class PlotXY(PamhyrPlot): kp = reach.geometry.get_kp() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) # Draw line for each profile @@ -98,9 +99,9 @@ class PlotXY(PamhyrPlot): markersize=3, marker='+' ) for x, y, kp in zip( - reach.geometry.get_x(), - reach.geometry.get_y(), - kp + reach.geometry.get_x(), + reach.geometry.get_y(), + kp ) ] diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 61e9f22b..15167fd3 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -59,6 +59,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class ResultsWindow(PamhyrWindow): _pamhyr_ui = "Results" _pamhyr_name = "Results" @@ -79,10 +80,10 @@ class ResultsWindow(PamhyrWindow): ) super(ResultsWindow, self).__init__( - title = name, - study = study, - config = config, - trad = ResultsTranslate(), + title=name, + study=study, + config=config, + trad=ResultsTranslate(), parent=parent ) @@ -97,11 +98,11 @@ class ResultsWindow(PamhyrWindow): for t in ["reach", "profile"]: table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( - table_view = table, - table_headers = self._trad.get_dict(f"table_headers_{t}"), - data = self._results, - undo = self._undo_stack, - opt_data = t + table_view=table, + table_headers=self._trad.get_dict(f"table_headers_{t}"), + data=self._results, + undo=self._undo_stack, + opt_data=t ) def setup_slider(self): @@ -125,12 +126,12 @@ class ResultsWindow(PamhyrWindow): self.plot_layout.addWidget(self.canvas) self.plot_xy = PlotXY( - canvas = self.canvas, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar, - display_current = False + canvas=self.canvas, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar, + display_current=False ) self.plot_xy.draw() @@ -144,11 +145,11 @@ class ResultsWindow(PamhyrWindow): self.plot_layout_2.addWidget(self.canvas_2) self.plot_kpc = PlotKPC( - canvas = self.canvas_2, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_2 + canvas=self.canvas_2, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_2 ) self.plot_kpc.draw() @@ -162,11 +163,11 @@ class ResultsWindow(PamhyrWindow): self.plot_layout_3.addWidget(self.canvas_3) self.plot_ac = PlotAC( - canvas = self.canvas_3, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_3 + canvas=self.canvas_3, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_3 ) self.plot_ac.draw() @@ -175,16 +176,17 @@ class ResultsWindow(PamhyrWindow): self.toolbar_4 = PamhyrPlotToolbar( self.canvas_4, self ) - self.plot_layout_4 = self.find(QVBoxLayout, "verticalLayout_hydrograph") + self.plot_layout_4 = self.find( + QVBoxLayout, "verticalLayout_hydrograph") self.plot_layout_4.addWidget(self.toolbar_4) self.plot_layout_4.addWidget(self.canvas_4) self.plot_h = PlotH( - canvas = self.canvas_4, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_4 + canvas=self.canvas_4, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_4 ) self.plot_h.draw() @@ -199,11 +201,11 @@ class ResultsWindow(PamhyrWindow): if self._study.river.has_sediment(): self.plot_sed_reach = PlotSedReach( - canvas = self.canvas_5, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_5 + canvas=self.canvas_5, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_5 ) self.plot_sed_reach.draw() @@ -212,17 +214,18 @@ class ResultsWindow(PamhyrWindow): self.toolbar_6 = PamhyrPlotToolbar( self.canvas_6, self ) - self.plot_layout_6 = self.find(QVBoxLayout, "verticalLayout_sed_profile") + self.plot_layout_6 = self.find( + QVBoxLayout, "verticalLayout_sed_profile") self.plot_layout_6.addWidget(self.toolbar_6) self.plot_layout_6.addWidget(self.canvas_6) if self._study.river.has_sediment(): self.plot_sed_profile = PlotSedProfile( - canvas = self.canvas_6, - results = self._results, - reach_id = 0, - profile_id = 0, - toolbar = self.toolbar_6 + canvas=self.canvas_6, + results=self._results, + reach_id=0, + profile_id=0, + toolbar=self.toolbar_6 ) self.plot_sed_profile.draw() @@ -283,7 +286,8 @@ class ResultsWindow(PamhyrWindow): self._table[t].dataChanged.connect(fun[t]) - self._slider_profile.valueChanged.connect(self._set_current_profile_slider) + self._slider_profile.valueChanged.connect( + self._set_current_profile_slider) self._slider_time.valueChanged.connect(self._set_current_timestamp) def update_table_selection_reach(self, ind): @@ -312,7 +316,7 @@ class ResultsWindow(PamhyrWindow): ) table.scrollTo(index) - def update(self, reach_id = None, profile_id = None, timestamp = None): + def update(self, reach_id=None, profile_id=None, timestamp=None): if reach_id is not None: self.plot_xy.set_reach(reach_id) self.plot_ac.set_reach(reach_id) @@ -364,7 +368,7 @@ class ResultsWindow(PamhyrWindow): if len(indexes) == 0: return - self.update(reach_id = indexes[0].row()) + self.update(reach_id=indexes[0].row()) def _set_current_profile(self): table = self.find(QTableView, f"tableView_profile") @@ -373,18 +377,18 @@ class ResultsWindow(PamhyrWindow): return ind = indexes[0].row() - self.update(profile_id = ind) + self.update(profile_id=ind) self._slider_profile.setValue(ind) def _set_current_profile_slider(self): pid = self._slider_profile.value() - self.update(profile_id = pid) + self.update(profile_id=pid) return def _set_current_timestamp(self): timestamp = self._timestamps[self._slider_time.value()] - self.update(timestamp = timestamp) + self.update(timestamp=timestamp) def _copy(self): logger.info("TODO: copy") diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py index 14f55ba3..ae9dcad1 100644 --- a/src/View/Results/translate.py +++ b/src/View/Results/translate.py @@ -22,6 +22,7 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class ResultsTranslate(PamhyrTranslate): def __init__(self): super(ResultsTranslate, self).__init__() diff --git a/src/View/RunSolver/Log/Window.py b/src/View/RunSolver/Log/Window.py index 2a33311f..3e343a45 100644 --- a/src/View/RunSolver/Log/Window.py +++ b/src/View/RunSolver/Log/Window.py @@ -44,22 +44,23 @@ from PyQt5.QtWidgets import ( _translate = QCoreApplication.translate + class SolverLogFileWindow(PamhyrWindow): _pamhyr_ui = "SolverLogFile" _pamhyr_name = "Solver logs" - def __init__(self, file_name = None, + def __init__(self, file_name=None, study=None, config=None, solver=None, parent=None): self._solver = solver self._file_name = file_name super(SolverLogFileWindow, self).__init__( - title = self._pamhyr_name, - study = study, - config = config, - options = [], - parent = parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) self.setup_action() @@ -75,7 +76,8 @@ class SolverLogFileWindow(PamhyrWindow): def setup_connections(self): self.find(QAction, "action_revert").triggered.connect(self.revert) - self.find(QAction, "action_open_in_editor").triggered.connect(self.open_on_editor) + self.find(QAction, "action_open_in_editor").triggered.connect( + self.open_on_editor) def setup_text(self): with open(self._file_name, "r") as f: diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 62484308..2adfce60 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -55,6 +55,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class SelectSolverWindow(PamhyrDialog): _pamhyr_ui = "SelectSolver" _pamhyr_name = "Select solver" @@ -64,11 +65,11 @@ class SelectSolverWindow(PamhyrDialog): self._solver = None super(SelectSolverWindow, self).__init__( - title = self._pamhyr_name, - study = study, - config = config, - options = [], - parent = parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) self.setup_combobox() @@ -101,6 +102,7 @@ class SelectSolverWindow(PamhyrDialog): super(SelectSolverWindow, self).accept() + class SolverLogWindow(PamhyrWindow): _pamhyr_ui = "SolverLog" _pamhyr_name = "Solver Log" @@ -111,11 +113,11 @@ class SolverLogWindow(PamhyrWindow): self._results = None super(SolverLogWindow, self).__init__( - title = self._pamhyr_name, - study = study, - config = config, - options = [], - parent = parent + title=self._pamhyr_name, + study=study, + config=config, + options=[], + parent=parent ) self.setup_action() @@ -132,22 +134,22 @@ class SolverLogWindow(PamhyrWindow): self._study.name.replace(" ", "_"), self._solver.name.replace(" ", "_"), ) - os.makedirs(self._workdir, exist_ok = True) + 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._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 + process=self._process, + output_queue=self._output ) def new_process(self, parent): @@ -206,7 +208,8 @@ class SolverLogWindow(PamhyrWindow): # Get results if self._results is None: - self._results = self._solver.results(self._study, self._workdir, qlog = self._output) + self._results = self._solver.results( + self._study, self._workdir, qlog=self._output) self._parent.set_results(self._solver, self._results) while self._output.qsize() != 0: @@ -219,14 +222,14 @@ class SolverLogWindow(PamhyrWindow): def start(self): if self._solver.is_stoped(): self._log(f" *** Export study {self._solver.name}", color="blue") - self._solver.export(self._study, self._workdir, qlog = self._output) + self._solver.export(self._study, self._workdir, qlog=self._output) self.update() self._process = self.new_process(self._parent) self._log(" *** Start", color="blue") self._results = None - self._solver.start(self._study, process = self._process) + self._solver.start(self._study, process=self._process) self.find(QAction, "action_start").setEnabled(False) if _signal: @@ -259,7 +262,8 @@ class SolverLogWindow(PamhyrWindow): def results(self): if self._results is None: - self._results = self._solver.results(self._study, self._workdir, qlog = self._output) + self._results = self._solver.results( + self._study, self._workdir, qlog=self._output) self._parent.set_results(self._solver, self._results) self._parent.open_solver_results(self._solver, self._results) @@ -267,10 +271,10 @@ class SolverLogWindow(PamhyrWindow): def log_file(self): file_name = os.path.join(self._workdir, self._solver.log_file()) log = SolverLogFileWindow( - file_name = file_name, - study = self._study, - config = self._config, - solver = self._solver, - parent = self, + file_name=file_name, + study=self._study, + config=self._config, + solver=self._solver, + parent=self, ) log.show() diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index a6fa2185..9a9baed3 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -9,15 +9,16 @@ from View.Tools.PamhyrPlot import PamhyrPlot logger = logging.getLogger() + class Plot(PamhyrPlot): def __init__(self, canvas=None, trad=None, data=None, toolbar=None, display_current=True, parent=None): super(Plot, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self._display_current = display_current @@ -39,13 +40,13 @@ class Plot(PamhyrPlot): if self.data is None: return - x = [0,1] - z = [0,0] + x = [0, 1] + z = [0, 0] sl = self.data.height() names = ["bottom"] + self.data.names() self.canvas.axes.set_xlim( - left = min(x), right = max(x) + left=min(x), right=max(x) ) lsl = [] @@ -75,7 +76,8 @@ class Plot(PamhyrPlot): lw=1.8, color='grey' if i == len(names) - 1 else None ) - self.canvas.axes.text(x[0] + 0.01, zsl[0] + 0.01, f'{names[-(i+1)]}') + self.canvas.axes.text( + x[0] + 0.01, zsl[0] + 0.01, f'{names[-(i+1)]}') self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py index 298fa529..e353211c 100644 --- a/src/View/SedimentLayers/Edit/UndoCommand.py +++ b/src/View/SedimentLayers/Edit/UndoCommand.py @@ -10,6 +10,7 @@ from PyQt5.QtWidgets import ( from Model.SedimentLayer.SedimentLayer import SedimentLayer from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + class SetNameCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -25,6 +26,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).name = self._new + class SetTypeCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -40,6 +42,7 @@ class SetTypeCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).type = self._new + class SetHeightCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -55,6 +58,7 @@ class SetHeightCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).height = self._new + class SetD50Command(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -70,6 +74,7 @@ class SetD50Command(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).d50 = self._new + class SetSigmaCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -85,6 +90,7 @@ class SetSigmaCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).sigma = self._new + class SetCriticalConstraintCommand(QUndoCommand): def __init__(self, sediment_layers, index, new_value): QUndoCommand.__init__(self) @@ -100,6 +106,7 @@ class SetCriticalConstraintCommand(QUndoCommand): def redo(self): self._sediment_layers.get(self._index).critical_constraint = self._new + class AddCommand(QUndoCommand): def __init__(self, sediment_layers, index): QUndoCommand.__init__(self) @@ -117,6 +124,7 @@ class AddCommand(QUndoCommand): else: self._sediment_layers.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, sediment_layers, rows): QUndoCommand.__init__(self) @@ -136,6 +144,7 @@ class DelCommand(QUndoCommand): def redo(self): self._sediment_layers.delete_i(self._rows) + class MoveCommand(QUndoCommand): def __init__(self, sediment_layers, up, i): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index e5835c97..080eee3f 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -51,11 +51,12 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class EditSedimentLayersWindow(PamhyrWindow): _pamhyr_ui = "EditSedimentLayers" _pamhyr_name = "Edit Sediment Layers" - def __init__(self, study=None, config = None, + def __init__(self, study=None, config=None, sl=None, parent=None): self._sl = sl @@ -68,11 +69,11 @@ class EditSedimentLayersWindow(PamhyrWindow): ) super(EditSedimentLayersWindow, self).__init__( - title = name, - study = study, - config = config, - trad = SedimentEditTranslate(), - parent = parent + title=name, + study=study, + config=config, + trad=SedimentEditTranslate(), + parent=parent ) self.setup_table() @@ -83,12 +84,12 @@ class EditSedimentLayersWindow(PamhyrWindow): table_headers = self._trad.get_dict("table_headers") table = self.find(QTableView, f"tableView") self._table = TableModel( - table_view = table, - table_headers = table_headers, - editable_headers = list(table_headers), - data = self._sl, - trad = self._trad, - undo = self._undo_stack, + table_view=table, + table_headers=table_headers, + editable_headers=list(table_headers), + data=self._sl, + trad=self._trad, + undo=self._undo_stack, ) table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -108,20 +109,20 @@ class EditSedimentLayersWindow(PamhyrWindow): def _set_plot(self): self.plot = Plot( - canvas = self.canvas, - data = self._sl, - toolbar = self.toolbar, - trad = self._trad, - display_current = False + canvas=self.canvas, + data=self._sl, + toolbar=self.toolbar, + trad=self._trad, + display_current=False ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_move_up").triggered.connect(self.move_up) - self.find(QAction, "action_move_down").triggered.connect(self.move_down) + self.find(QAction, "action_move_down").triggered.connect( + self.move_down) self._table.dataChanged.connect(self._set_plot) self._table.layoutChanged.connect(self._set_plot) diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py index 658d4f23..ce036b8d 100644 --- a/src/View/SedimentLayers/Edit/translate.py +++ b/src/View/SedimentLayers/Edit/translate.py @@ -6,6 +6,7 @@ from View.SedimentLayers.translate import SedimentTranslate _translate = QCoreApplication.translate + class SedimentEditTranslate(SedimentTranslate): def __init__(self): super(SedimentEditTranslate, self).__init__() diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py index 069a09d7..0db3b4e2 100644 --- a/src/View/SedimentLayers/Reach/Plot.py +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -15,16 +15,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class Plot(PamhyrPlot): def __init__(self, data=None, display_current=True, canvas=None, trad=None, toolbar=None, parent=None): super(Plot, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self._display_current = display_current @@ -58,7 +59,7 @@ class Plot(PamhyrPlot): z_max = self.data.get_z_max() self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left=min(kp), right=max(kp) ) # Compute sediment layer in function to profile z_min diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py index 01b2fe24..062070ac 100644 --- a/src/View/SedimentLayers/Reach/Profile/Plot.py +++ b/src/View/SedimentLayers/Reach/Profile/Plot.py @@ -15,16 +15,17 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class Plot(PamhyrPlot): def __init__(self, data=None, display_current=True, canvas=None, trad=None, toolbar=None, parent=None): super(Plot, self).__init__( - canvas = canvas, - trad = trad, - data = data, - toolbar = toolbar, - parent = parent + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent ) self._display_current = display_current @@ -57,7 +58,7 @@ class Plot(PamhyrPlot): sl = self.data.get_sl() self.canvas.axes.set_xlim( - left = min(x), right = max(x) + left=min(x), right=max(x) ) # Compute sediment layer in function to point z diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py index 3550940b..fbdfb042 100644 --- a/src/View/SedimentLayers/Reach/Profile/Table.py +++ b/src/View/SedimentLayers/Reach/Profile/Table.py @@ -26,6 +26,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class ComboBoxDelegate(QItemDelegate): def __init__(self, study=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) diff --git a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py index fe3497ae..980d970b 100644 --- a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py @@ -16,6 +16,7 @@ from Model.SedimentLayer.SedimentLayerList import SedimentLayerList logger = logging.getLogger() + class SetSLCommand(QUndoCommand): def __init__(self, profile, index, new_value): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index e2c2464b..1dcbd0a8 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -37,6 +37,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class ProfileSedimentLayersWindow(PamhyrWindow): _pamhyr_ui = "ProfileSedimentLayers" _pamhyr_name = "Profile sediment layers" @@ -49,11 +50,11 @@ class ProfileSedimentLayersWindow(PamhyrWindow): name = self.compute_name(study) super(ProfileSedimentLayersWindow, self).__init__( - title = name, - study = study, - config = config, - trad = SedimentProfileTranslate(), - parent = parent + title=name, + study=study, + config=config, + trad=SedimentProfileTranslate(), + parent=parent ) self.setup_table() @@ -72,7 +73,7 @@ class ProfileSedimentLayersWindow(PamhyrWindow): pname = _translate( "SedimentLayers", "(no name - @kp)").replace("@kp", str(self._profile.kp) - ) + ) return ( self._pamhyr_name + " - " @@ -84,20 +85,20 @@ class ProfileSedimentLayersWindow(PamhyrWindow): table_headers = self._trad.get_dict("table_headers") self._delegate_sl = ComboBoxDelegate( - study = self._study, + study=self._study, parent=self ) table = self.find(QTableView, f"tableView") self._table = TableModel( - table_view = table, - data = self._profile, - opt_data = self._study, - table_headers = table_headers, - editable_headers = ["sl"], - delegates = {"sl": self._delegate_sl}, - trad = self._trad, - undo = self._undo_stack, + table_view=table, + data=self._profile, + opt_data=self._study, + table_headers=table_headers, + editable_headers=["sl"], + delegates={"sl": self._delegate_sl}, + trad=self._trad, + undo=self._undo_stack, ) table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -118,10 +119,10 @@ class ProfileSedimentLayersWindow(PamhyrWindow): def _update_plot(self): self.plot = Plot( - canvas = self.canvas, - data = self._profile, - toolbar = self.toolbar, - display_current = False + canvas=self.canvas, + data=self._profile, + toolbar=self.toolbar, + display_current=False ) self.plot.draw() diff --git a/src/View/SedimentLayers/Reach/Profile/translate.py b/src/View/SedimentLayers/Reach/Profile/translate.py index b1c12405..81e6623d 100644 --- a/src/View/SedimentLayers/Reach/Profile/translate.py +++ b/src/View/SedimentLayers/Reach/Profile/translate.py @@ -6,6 +6,7 @@ from View.SedimentLayers.Reach.translate import SedimentReachTranslate _translate = QCoreApplication.translate + class SedimentProfileTranslate(SedimentReachTranslate): def __init__(self): super(SedimentProfileTranslate, self).__init__() diff --git a/src/View/SedimentLayers/Reach/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py index c8a1efa0..9f3c878b 100644 --- a/src/View/SedimentLayers/Reach/SLDialog.py +++ b/src/View/SedimentLayers/Reach/SLDialog.py @@ -19,15 +19,16 @@ from View.SedimentLayers.Reach.translate import * _translate = QCoreApplication.translate + class SLDialog(PamhyrDialog): _pamhyr_ui = "SLDialog" _pamhyr_name = "SL" def __init__(self, study=None, config=None, parent=None): super(SLDialog, self).__init__( - title = self._pamhyr_name, - study = study, - config = config, + title=self._pamhyr_name, + study=study, + config=config, parent=parent ) diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py index 250a4735..7e38fcc8 100644 --- a/src/View/SedimentLayers/Reach/Table.py +++ b/src/View/SedimentLayers/Reach/Table.py @@ -25,6 +25,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class ComboBoxDelegate(QItemDelegate): def __init__(self, study=None, trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) diff --git a/src/View/SedimentLayers/Reach/UndoCommand.py b/src/View/SedimentLayers/Reach/UndoCommand.py index 17e1f0f1..4bce2235 100644 --- a/src/View/SedimentLayers/Reach/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/UndoCommand.py @@ -16,6 +16,7 @@ from Model.SedimentLayer.SedimentLayerList import SedimentLayerList logger = logging.getLogger() + class SetSLCommand(QUndoCommand): def __init__(self, reach, index, new_value): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 829bf035..9c400909 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -39,6 +39,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class ReachSedimentLayersWindow(PamhyrWindow): _pamhyr_ui = "ReachSedimentLayers" _pamhyr_name = "Reach sediment layers" @@ -54,11 +55,11 @@ class ReachSedimentLayersWindow(PamhyrWindow): ) super(ReachSedimentLayersWindow, self).__init__( - title = name, - study = study, - config = config, - trad = SedimentReachTranslate(), - parent = parent + title=name, + study=study, + config=config, + trad=SedimentReachTranslate(), + parent=parent ) self.setup_table() @@ -69,21 +70,21 @@ class ReachSedimentLayersWindow(PamhyrWindow): table_headers = self._trad.get_dict("table_headers") self._delegate_sl = ComboBoxDelegate( - study = self._study, - trad = self._trad, + study=self._study, + trad=self._trad, parent=self ) table = self.find(QTableView, f"tableView") self._table = TableModel( - table_view = table, - data = self._reach, - opt_data = self._study, - table_headers = table_headers, - editable_headers = ["sl"], - delegates = {"sl": self._delegate_sl}, - trad = self._trad, - undo = self._undo_stack, + table_view=table, + data=self._reach, + opt_data=self._study, + table_headers=table_headers, + editable_headers=["sl"], + delegates={"sl": self._delegate_sl}, + trad=self._trad, + undo=self._undo_stack, ) table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -104,15 +105,14 @@ class ReachSedimentLayersWindow(PamhyrWindow): def _update_plot(self): self.plot = Plot( - canvas = self.canvas, - data = self._reach, - toolbar = self.toolbar, - trad = self._trad, - display_current = False + canvas=self.canvas, + data=self._reach, + toolbar=self.toolbar, + trad=self._trad, + display_current=False ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_edit").triggered.connect(self.edit_profile) @@ -128,7 +128,6 @@ class ReachSedimentLayersWindow(PamhyrWindow): self._table.dataChanged\ .connect(self._update_plot) - def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( @@ -157,8 +156,8 @@ class ReachSedimentLayersWindow(PamhyrWindow): def apply_sl_each_profile(self): slw = SLDialog( - study = self._study, - parent = self + study=self._study, + parent=self ) if slw.exec(): sl = slw.sl @@ -171,16 +170,15 @@ class ReachSedimentLayersWindow(PamhyrWindow): for row in rows: slw = ProfileSedimentLayersWindow( - study = self._study, - profile = self._reach.profile(row), - parent = self + study=self._study, + profile=self._reach.profile(row), + parent=self ) slw.show() - def edit_sl(self): slw = SedimentLayersWindow( - study = self._study, - parent = self + study=self._study, + parent=self ) slw.show() diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py index 34785f32..fd2d3bdb 100644 --- a/src/View/SedimentLayers/Reach/translate.py +++ b/src/View/SedimentLayers/Reach/translate.py @@ -6,6 +6,7 @@ from View.SedimentLayers.translate import SedimentTranslate _translate = QCoreApplication.translate + class SedimentReachTranslate(SedimentTranslate): def __init__(self): super(SedimentReachTranslate, self).__init__() diff --git a/src/View/SedimentLayers/UndoCommand.py b/src/View/SedimentLayers/UndoCommand.py index f785743b..379a9545 100644 --- a/src/View/SedimentLayers/UndoCommand.py +++ b/src/View/SedimentLayers/UndoCommand.py @@ -10,6 +10,7 @@ from PyQt5.QtWidgets import ( from Model.SedimentLayer.SedimentLayer import SedimentLayer from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + class SetNameCommand(QUndoCommand): def __init__(self, sediment_layers_list, index, new_value): QUndoCommand.__init__(self) @@ -25,6 +26,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._sediment_layers_list.get(self._index).name = self._new + class SetCommentCommand(QUndoCommand): def __init__(self, sediment_layers_list, index, new_value): QUndoCommand.__init__(self) @@ -40,6 +42,7 @@ class SetCommentCommand(QUndoCommand): def redo(self): self._sediment_layers_list.get(self._index).comment = self._new + class AddCommand(QUndoCommand): def __init__(self, sediment_layers_list, index): QUndoCommand.__init__(self) @@ -57,6 +60,7 @@ class AddCommand(QUndoCommand): else: self._sediment_layers_list.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, sediment_layers_list, rows): QUndoCommand.__init__(self) @@ -76,6 +80,7 @@ class DelCommand(QUndoCommand): def redo(self): self._sediment_layers_list.delete_i(self._rows) + class MoveCommand(QUndoCommand): def __init__(self, sediment_layers_list, up, i): QUndoCommand.__init__(self) diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index 3c394de0..1ff2c7f9 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -51,6 +51,7 @@ from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow logger = logging.getLogger() + class SedimentLayersWindow(PamhyrWindow): _pamhyr_ui = "SedimentLayersList" _pamhyr_name = "Sediment Layers List" @@ -63,10 +64,10 @@ class SedimentLayersWindow(PamhyrWindow): ) super(SedimentLayersWindow, self).__init__( - title = name, - study = study, - config = config, - trad = SedimentTranslate(), + title=name, + study=study, + config=config, + trad=SedimentTranslate(), parent=parent ) @@ -77,12 +78,12 @@ class SedimentLayersWindow(PamhyrWindow): def setup_table(self): table = self.find(QTableView, f"tableView") self._table = TableModel( - table_view = table, - table_headers = self._trad.get_dict("table_headers"), - editable_headers = ["name", "comment"], - data = self._study.river.sediment_layers, - trad = self._trad, - undo = self._undo_stack, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "comment"], + data=self._study.river.sediment_layers, + trad=self._trad, + undo=self._undo_stack, ) table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -98,7 +99,8 @@ class SedimentLayersWindow(PamhyrWindow): def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) - self.find(QAction, "action_edit").triggered.connect(self.edit_sediment_layers) + self.find(QAction, "action_edit").triggered.connect( + self.edit_sediment_layers) table = self.find(QTableView, f"tableView") table.selectionModel()\ @@ -115,15 +117,14 @@ class SedimentLayersWindow(PamhyrWindow): return self.plot = Plot( - canvas = self.canvas, - data = self._sediment_layers.get(rows[0]), - trad = self._trad, - toolbar = None, - display_current = False + canvas=self.canvas, + data=self._sediment_layers.get(rows[0]), + trad=self._trad, + toolbar=None, + display_current=False ) self.plot.draw() - def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( @@ -167,8 +168,8 @@ class SedimentLayersWindow(PamhyrWindow): for row in rows: slw = EditSedimentLayersWindow( - study = self._study, - sl = self._sediment_layers.get(row), - parent = self + study=self._study, + sl=self._sediment_layers.get(row), + parent=self ) slw.show() diff --git a/src/View/SedimentLayers/translate.py b/src/View/SedimentLayers/translate.py index 42d59f92..83a6169d 100644 --- a/src/View/SedimentLayers/translate.py +++ b/src/View/SedimentLayers/translate.py @@ -6,6 +6,7 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class SedimentTranslate(PamhyrTranslate): def __init__(self): super(SedimentTranslate, self).__init__() diff --git a/src/View/SolverParameters/Table.py b/src/View/SolverParameters/Table.py index 47fa0ae9..bbf12ada 100644 --- a/src/View/SolverParameters/Table.py +++ b/src/View/SolverParameters/Table.py @@ -44,6 +44,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate + class TableModel(PamhyrTableModel): def _setup_lst(self): self._tab = self._opt_data diff --git a/src/View/SolverParameters/UndoCommand.py b/src/View/SolverParameters/UndoCommand.py index d3343d00..2b6804b8 100644 --- a/src/View/SolverParameters/UndoCommand.py +++ b/src/View/SolverParameters/UndoCommand.py @@ -25,6 +25,7 @@ from PyQt5.QtWidgets import ( from Model.SolverParameters.SolverParametersList import SolverParametersList + class SetCommand(QUndoCommand): def __init__(self, data, index, column, new_value): QUndoCommand.__init__(self) diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index b655cd53..d7100193 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -50,6 +50,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class SolverParametersWindow(PamhyrWindow): _pamhyr_ui = "SolverParameters" _pamhyr_name = "Solver parameters" @@ -57,12 +58,12 @@ class SolverParametersWindow(PamhyrWindow): def __init__(self, study=None, config=None, parent=None): name = self._pamhyr_name + " - " + study.name super(SolverParametersWindow, self).__init__( - title = name, - study = study, - config = config, - options = ['copy'], - trad = ParamTranslate(), - parent = parent + title=name, + study=study, + config=config, + options=['copy'], + trad=ParamTranslate(), + parent=parent ) self._params = self._study.river.parameters @@ -96,13 +97,13 @@ class SolverParametersWindow(PamhyrWindow): # Create table model self._table[st] = TableModel( - table_view = table, - data = self._study.river, - opt_data = st, - table_headers = self._trad.get_dict("table_headers"), - editable_headers = ["value"], - trad = self._trad, - undo = self._undo_stack[st], + table_view=table, + data=self._study.river, + opt_data=st, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["value"], + trad=self._trad, + undo=self._undo_stack[st], ) table.setModel(self._table[st]) diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 01c51a70..16816308 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -22,6 +22,7 @@ from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate + class ParamTranslate(PamhyrTranslate): def __init__(self): super(ParamTranslate, self).__init__() diff --git a/src/View/Stricklers/Table.py b/src/View/Stricklers/Table.py index 58700803..7b740b91 100644 --- a/src/View/Stricklers/Table.py +++ b/src/View/Stricklers/Table.py @@ -46,6 +46,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate + class TableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: diff --git a/src/View/Stricklers/UndoCommand.py b/src/View/Stricklers/UndoCommand.py index 52988b26..00aedb21 100644 --- a/src/View/Stricklers/UndoCommand.py +++ b/src/View/Stricklers/UndoCommand.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from Model.LateralContribution.LateralContribution import LateralContribution from Model.LateralContribution.LateralContributionList import LateralContributionList + class SetNameCommand(QUndoCommand): def __init__(self, data, index, new_value): QUndoCommand.__init__(self) @@ -41,6 +42,7 @@ class SetNameCommand(QUndoCommand): def redo(self): self._data.get(self._index).name = self._new + class SetCommentCommand(QUndoCommand): def __init__(self, data, index, new_value): QUndoCommand.__init__(self) @@ -56,6 +58,7 @@ class SetCommentCommand(QUndoCommand): def redo(self): self._data.get(self._index).comment = self._new + class SetMinorCommand(QUndoCommand): def __init__(self, data, index, new_value): QUndoCommand.__init__(self) @@ -71,6 +74,7 @@ class SetMinorCommand(QUndoCommand): def redo(self): self._data.get(self._index).minor = self._new + class SetMediumCommand(QUndoCommand): def __init__(self, data, index, new_value): QUndoCommand.__init__(self) @@ -104,6 +108,7 @@ class AddCommand(QUndoCommand): else: self._data.insert(self._index, self._new) + class DelCommand(QUndoCommand): def __init__(self, data, rows): QUndoCommand.__init__(self) @@ -123,6 +128,7 @@ class DelCommand(QUndoCommand): def redo(self): self._data.delete_i(self._rows) + class SortCommand(QUndoCommand): def __init__(self, data, _reverse): QUndoCommand.__init__(self) @@ -150,6 +156,7 @@ class SortCommand(QUndoCommand): ) self._old = None + class PasteCommand(QUndoCommand): def __init__(self, data, row, el): QUndoCommand.__init__(self) diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index 792ed4e3..b647154c 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -49,6 +49,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() + class StricklersWindow(PamhyrWindow): _pamhyr_ui = "Stricklers" _pamhyr_name = "Stricklers" @@ -57,11 +58,11 @@ class StricklersWindow(PamhyrWindow): name = self._pamhyr_name + " - " + study.name super(StricklersWindow, self).__init__( - title = name, - study = study, - config = config, - trad = StricklersTranslate(), - parent = parent + title=name, + study=study, + config=config, + trad=StricklersTranslate(), + parent=parent ) self.setup_table() @@ -78,11 +79,11 @@ class StricklersWindow(PamhyrWindow): data = self._config.stricklers self._table[t] = TableModel( - table_view = table, - table_headers = self._trad.get_dict("table_headers"), - editable_headers = ["name", "comment", "minor", "medium"], - data = data, - undo = self._undo_stack, + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "comment", "minor", "medium"], + data=data, + undo=self._undo_stack, ) table.setModel(self._table[t]) @@ -106,7 +107,6 @@ class StricklersWindow(PamhyrWindow): ) ) - def add(self): rows = self.index_selected_rows() if len(rows) == 0: diff --git a/src/View/Study/Window.py b/src/View/Study/Window.py index 871d0545..35bbe1d9 100644 --- a/src/View/Study/Window.py +++ b/src/View/Study/Window.py @@ -27,27 +27,29 @@ from PyQt5.QtWidgets import ( _translate = QCoreApplication.translate + class NewStudyWindow(PamhyrDialog): _pamhyr_ui = "NewStudy" _pamhyr_name = "Edit/New Study" - def __init__(self, study=None, config = None, parent=None): + def __init__(self, study=None, config=None, parent=None): if study is not None: name = f"Edit study - {study.name}" else: name = "New study" super(NewStudyWindow, self).__init__( - title = name, - study = study, - config = config, - options = [], - parent = parent + title=name, + study=study, + config=config, + options=[], + parent=parent ) if not self._study is None: self.set_line_edit_text("lineEdit_name", self._study.name) - self.set_text_edit_text("textEdit_description", self._study.description) + self.set_text_edit_text( + "textEdit_description", self._study.description) self.set_datetime_edit("dateTimeEdit_date", self._study.date) self.find(QLabel, "label_creation_date_data").setText( @@ -81,7 +83,6 @@ class NewStudyWindow(PamhyrDialog): self.find(QLabel, "label_date").setEnabled(True) self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True) - def accept(self): name = self.get_line_edit_text("lineEdit_name") description = self.get_text_edit_text("textEdit_description") diff --git a/src/View/Tools/ASubWindow.py b/src/View/Tools/ASubWindow.py index b6adcc82..aa95ddcd 100644 --- a/src/View/Tools/ASubWindow.py +++ b/src/View/Tools/ASubWindow.py @@ -46,6 +46,7 @@ from Model.Except import ClipboardFormatError logger = logging.getLogger() + class WindowToolKit(object): def __init__(self, parent=None): super(WindowToolKit, self).__init__() @@ -150,7 +151,7 @@ class ASubWindowFeatures(object): return qtype - def get_label_text(self, name:str): + def get_label_text(self, name: str): """Get text of label component Args: @@ -161,7 +162,7 @@ class ASubWindowFeatures(object): """ return self.find(QLabel, name).text() - def set_label_text(self, name:str, text:str): + def set_label_text(self, name: str, text: str): """Set text of label component Args: @@ -173,8 +174,7 @@ class ASubWindowFeatures(object): """ self.find(QLabel, name).setText(text) - - def set_line_edit_text(self, name:str, text:str): + def set_line_edit_text(self, name: str, text: str): """Set text of line edit component Args: @@ -189,7 +189,7 @@ class ASubWindowFeatures(object): except AttributeError as e: logger.error(e) - def get_line_edit_text(self, name:str): + def get_line_edit_text(self, name: str): """Get text of line edit component Args: @@ -200,7 +200,7 @@ class ASubWindowFeatures(object): """ return self.find(QLineEdit, name).text() - def set_text_edit_text(self, name:str, text:str): + def set_text_edit_text(self, name: str, text: str): """Set text of text edit component Args: @@ -212,7 +212,7 @@ class ASubWindowFeatures(object): """ self.find(QTextEdit, name).setText(text) - def get_text_edit_text(self, name:str): + def get_text_edit_text(self, name: str): """Get text of text edit component Args: @@ -223,7 +223,7 @@ class ASubWindowFeatures(object): """ return self.find(QTextEdit, name).toHtml() - def set_plaintext_edit_text(self, name:str, text:str): + def set_plaintext_edit_text(self, name: str, text: str): """Set text of text edit component Args: @@ -235,7 +235,7 @@ class ASubWindowFeatures(object): """ self.find(QPlainTextEdit, name).setPlainText(text) - def get_plaintext_edit_text(self, name:str): + def get_plaintext_edit_text(self, name: str): """Get text of text edit component Args: @@ -246,7 +246,7 @@ class ASubWindowFeatures(object): """ return self.find(QPlainTextEdit, name).toPlainText() - def set_check_box(self, name:str, checked:bool): + def set_check_box(self, name: str, checked: bool): """Set status of checkbox component Args: @@ -258,7 +258,7 @@ class ASubWindowFeatures(object): """ self.find(QCheckBox, name).setChecked(checked) - def get_check_box(self, name:str): + def get_check_box(self, name: str): """Get status of checkbox component Args: @@ -269,8 +269,7 @@ class ASubWindowFeatures(object): """ return self.find(QCheckBox, name).isChecked() - - def set_time_edit(self, name:str, time:str): + def set_time_edit(self, name: str, time: str): """Set time of timeedit component Args: @@ -283,7 +282,7 @@ class ASubWindowFeatures(object): qtime = QTime.fromString(time) self.find(QTimeEdit, name).setTime(qtime) - def get_time_edit(self, name:str): + def get_time_edit(self, name: str): """Get time of timeedit component Args: @@ -294,7 +293,7 @@ class ASubWindowFeatures(object): """ return self.find(QTimeEdit, name).time().toString() - def set_spin_box(self, name:str, value:int): + def set_spin_box(self, name: str, value: int): """Set value of spinbox component Args: @@ -306,7 +305,7 @@ class ASubWindowFeatures(object): """ self.find(QSpinBox, name).setValue(value) - def get_spin_box(self, name:str): + def get_spin_box(self, name: str): """Get time of spin box component Args: @@ -317,7 +316,7 @@ class ASubWindowFeatures(object): """ return self.find(QSpinBox, name).value() - def set_action_checkable(self, name:str, checked:bool): + def set_action_checkable(self, name: str, checked: bool): """Set value of action Args: @@ -329,7 +328,7 @@ class ASubWindowFeatures(object): """ self.find(QAction, name).setChecked(checked) - def get_action_checkable(self, name:str): + def get_action_checkable(self, name: str): """Get status of action Args: @@ -340,8 +339,7 @@ class ASubWindowFeatures(object): """ return self.find(QAction, name).isChecked() - - def set_push_button_checkable(self, name:str, checked:bool): + def set_push_button_checkable(self, name: str, checked: bool): """Set value of push button component Args: @@ -353,7 +351,7 @@ class ASubWindowFeatures(object): """ self.find(QPushButton, name).setChecked(checked) - def get_push_button_checkable(self, name:str): + def get_push_button_checkable(self, name: str): """Get status of push button Args: @@ -364,7 +362,7 @@ class ASubWindowFeatures(object): """ return self.find(QPushButton, name).isChecked() - def set_radio_button(self, name:str, checked:bool): + def set_radio_button(self, name: str, checked: bool): """Set value of radio button component Args: @@ -376,7 +374,7 @@ class ASubWindowFeatures(object): """ self.find(QRadioButton, name).setChecked(checked) - def get_radio_button(self, name:str): + def get_radio_button(self, name: str): """Get status of radio button Args: @@ -387,7 +385,7 @@ class ASubWindowFeatures(object): """ return self.find(QRadioButton, name).isChecked() - def combobox_add_item(self, name:str, item:str): + def combobox_add_item(self, name: str, item: str): """Add item in combo box Args: @@ -399,7 +397,7 @@ class ASubWindowFeatures(object): """ self.find(QComboBox, name).addItem(item) - def combobox_add_items(self, name:str, items:str): + def combobox_add_items(self, name: str, items: str): """Add item in combo box Args: @@ -412,7 +410,7 @@ class ASubWindowFeatures(object): for item in items: self.find(QComboBox, name).addItem(item) - def set_combobox_text(self, name:str, item:str): + def set_combobox_text(self, name: str, item: str): """Set current text of combo box Args: @@ -424,7 +422,7 @@ class ASubWindowFeatures(object): """ self.find(QComboBox, name).setCurrentText(item) - def get_combobox_text(self, name:str): + def get_combobox_text(self, name: str): """Get current text of combo box Args: @@ -435,7 +433,7 @@ class ASubWindowFeatures(object): """ return self.find(QComboBox, name).currentText() - def get_datetime_edit(self, name:str): + def get_datetime_edit(self, name: str): """Get datetime of datetime edit Args: @@ -446,7 +444,7 @@ class ASubWindowFeatures(object): """ return self.find(QDateTimeEdit, name).dateTime().toPyDateTime() - def set_datetime_edit(self, name:str, date:datetime): + def set_datetime_edit(self, name: str, date: datetime): """Set datetime of a datetime edit Args: @@ -500,6 +498,7 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): return self.ui.findChild(qtype, name) + class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubWindow, self).__init__(parent=parent) @@ -536,6 +535,7 @@ class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): return self.ui.findChild(qtype, name) + class AWidget(QWidget, ASubWindowFeatures): def __init__(self, ui="", parent=None): super(AWidget, self).__init__(parent=parent) diff --git a/src/View/Tools/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py index 6c93b84e..5a6c132c 100644 --- a/src/View/Tools/ListedSubWindow.py +++ b/src/View/Tools/ListedSubWindow.py @@ -21,6 +21,7 @@ from functools import reduce logger = logging.getLogger() + class ListedSubWindow(object): def __init__(self, **kwargs): super(ListedSubWindow, self).__init__() @@ -37,7 +38,8 @@ class ListedSubWindow(object): self.sub_win_list.append((name, win)) self.sub_win_cnt += 1 try: - logger.info(f"Open window: {name}: {self.sub_win_cnt}: {win.hash()}") + logger.info( + f"Open window: {name}: {self.sub_win_cnt}: {win.hash()}") except: logger.info(f"Open window: {name}: {self.sub_win_cnt}: X") @@ -75,7 +77,7 @@ class ListedSubWindow(object): False ) - def sub_win_exists(self, name, contain = []): + def sub_win_exists(self, name, contain=[]): if contain == []: return self._sub_win_exists(name) else: diff --git a/src/View/Tools/PamhyrDelegate.py b/src/View/Tools/PamhyrDelegate.py index c5e97da3..c253ec25 100644 --- a/src/View/Tools/PamhyrDelegate.py +++ b/src/View/Tools/PamhyrDelegate.py @@ -32,6 +32,7 @@ from View.Tools.PamhyrWidget import ( logger = logging.getLogger() + class PamhyrExTimeDelegate(QItemDelegate): def __init__(self, data=None, mode="time", parent=None): super(PamhyrExTimeDelegate, self).__init__(parent) diff --git a/src/View/Tools/PamhyrPlot.py b/src/View/Tools/PamhyrPlot.py index b06ec2c2..600687d5 100644 --- a/src/View/Tools/PamhyrPlot.py +++ b/src/View/Tools/PamhyrPlot.py @@ -20,6 +20,7 @@ from View.Tools.Plot.APlot import APlot from View.Tools.Plot.PamhyrCanvas import MplCanvas from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + class PamhyrPlot(APlot): def __init__(self, data=None, trad=None, canvas=None, toolbar=None, @@ -32,7 +33,7 @@ class PamhyrPlot(APlot): self._toolbar = toolbar self._parent = parent - super(PamhyrPlot, self).__init__(data = data) + super(PamhyrPlot, self).__init__(data=data) @property def canvas(self): diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 43aa22c2..ff815624 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -57,6 +57,7 @@ class PamhyrTextDelegate(QStyledItemDelegate): def updateEditorGeometry(self, editor, option, index): editor.setGeometry(option.rect) + class PamhyrTableModel(QAbstractTableModel): def _setup_delegates(self): if self._table_view is None: @@ -75,14 +76,14 @@ class PamhyrTableModel(QAbstractTableModel): ) def __init__(self, - table_view = None, - table_headers = {}, - editable_headers = [], - delegates = {}, - trad = None, - data = None, - undo = None, - opt_data = None): + table_view=None, + table_headers={}, + editable_headers=[], + delegates={}, + trad=None, + data=None, + undo=None, + opt_data=None): super(PamhyrTableModel, self).__init__() self._table_view = table_view @@ -112,14 +113,13 @@ class PamhyrTableModel(QAbstractTableModel): self._table_view.setAlternatingRowColors(True) self._table_view.resizeColumnsToContents() - def flags(self, index): column = index.column() options = Qt.ItemIsEnabled | Qt.ItemIsSelectable if (self._editable_headers or - self._headers[column] in self._editable_headers): + self._headers[column] in self._editable_headers): options |= Qt.ItemIsEditable return options diff --git a/src/View/Tools/PamhyrWidget.py b/src/View/Tools/PamhyrWidget.py index f906beed..d3e02dd9 100644 --- a/src/View/Tools/PamhyrWidget.py +++ b/src/View/Tools/PamhyrWidget.py @@ -32,14 +32,15 @@ from View.Tools.ASubWindow import AWidget logger = logging.getLogger() + class PamhyrWidget(AWidget): _pamhyr_ui = None _pamhyr_name = "PamhyrWidget" def __init__(self, parent=None): super(PamhyrWidget, self).__init__( - ui = self._pamhyr_ui, - parent = parent + ui=self._pamhyr_ui, + parent=parent ) @@ -83,6 +84,7 @@ class PamhyrExtendedTimeEdit(AWidget): return timedelta(days=days, seconds=secs) + class PamhyrExtendedDateTimeEdit(AWidget): def __init__(self, parent=None): super(ExtendedDateTimeEdit, self).__init__( diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py index fb4bf1a1..a3c7d4c3 100644 --- a/src/View/Tools/PamhyrWindow.py +++ b/src/View/Tools/PamhyrWindow.py @@ -31,8 +31,9 @@ from View.Tools.ListedSubWindow import ListedSubWindow logger = logging.getLogger() + class PamhyrWindowTools(object): - def __init__(self, options = ["undo", "copy"], parent = None, **kwargs): + def __init__(self, options=["undo", "copy"], parent=None, **kwargs): super(PamhyrWindowTools, self).__init__() self._hash_data = [] @@ -139,10 +140,10 @@ class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): _pamhyr_name = "PamhyrWindow" def __init__(self, - title = "Pamhyr2", - study = None, config = None, trad = None, - options = ["undo", "copy"], - parent = None): + title="Pamhyr2", + study=None, config=None, trad=None, + options=["undo", "copy"], + parent=None): self._title = title self._study = study self._config = config @@ -150,9 +151,9 @@ class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): self._parent = parent super(PamhyrWindow, self).__init__( - name = self._pamhyr_name, - ui = self._pamhyr_ui, - parent = parent, + name=self._pamhyr_name, + ui=self._pamhyr_ui, + parent=parent, ) self._hash_data.append(self._study) @@ -166,10 +167,10 @@ class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools): _pamhyr_name = "PamhyrWindow" def __init__(self, - title = "Pamhyr2", - study = None, config = None, trad = None, - options = ["undo", "copy"], - parent = None): + title="Pamhyr2", + study=None, config=None, trad=None, + options=["undo", "copy"], + parent=None): self._title = title self._study = study self._config = config @@ -177,9 +178,9 @@ class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools): self._parent = parent super(PamhyrDialog, self).__init__( - name = self._pamhyr_name, - ui = self._pamhyr_ui, - parent = parent, + name=self._pamhyr_name, + ui=self._pamhyr_ui, + parent=parent, ) self._hash_data.append(self._study) diff --git a/src/View/Tools/Plot/APlot.py b/src/View/Tools/Plot/APlot.py index 7d8de307..3fe98fbc 100644 --- a/src/View/Tools/Plot/APlot.py +++ b/src/View/Tools/Plot/APlot.py @@ -18,6 +18,7 @@ from Model.Except import NotImplementedMethodeError + class APlot(object): def __init__(self, data=None): super(APlot, self).__init__() @@ -29,7 +30,6 @@ class APlot(object): def data(self): return self._data - def draw(self): """Draw plot @@ -38,8 +38,7 @@ class APlot(object): """ raise NotImplementedMethodeError(self, self.draw) - - def update(self, ind = None): + def update(self, ind=None): """Update plot Update the plot, update for data index IND if define, else diff --git a/src/View/Tools/Plot/OnPickEvent.py b/src/View/Tools/Plot/OnPickEvent.py index fdfa1de6..e02c897d 100644 --- a/src/View/Tools/Plot/OnPickEvent.py +++ b/src/View/Tools/Plot/OnPickEvent.py @@ -26,6 +26,7 @@ from shapely.geometry.polygon import Polygon as ShapelyPolygon logger = logging.getLogger() + class OnpickEvent(object): def __init__(self, ax, x, y, x_carto, y_carto, tableview=None): """ @@ -117,7 +118,8 @@ class OnpickEvent(object): self.pt1 = self.ax.plot(self.x[row], self.y[row], '+', c='Blue', markersize=7) self.pt.append(self.pt1) - self.update_select_point_point_bis(self.x[row], self.y[row]) + self.update_select_point_point_bis( + self.x[row], self.y[row]) elif len(rows) == 1: for row in rows: try: @@ -130,7 +132,8 @@ class OnpickEvent(object): self.update_select_point_point_bis(self.x[row], self.y[row]) except: - logger.info("update_select_point_point_bis: Update issue, possible index missing") + logger.info( + "update_select_point_point_bis: Update issue, possible index missing") self.ax.figure.canvas.draw_idle() @@ -382,7 +385,8 @@ class OnpickEvent(object): self.pos_x_annotation = x_min + ((x_max - x_min) / 2) percent = 0 - y_ecran_lim = ((max(self.ax.set_ylim()) - min(self.ax.set_ylim())) / 2) + y_ecran_lim = ((max(self.ax.set_ylim()) - + min(self.ax.set_ylim())) / 2) if abs(y_ecran_lim) > 4: percent = 0.05 @@ -499,7 +503,8 @@ class OnpickEvent(object): liste_points_miroir = [ x for (x, y) in zip(xs, ys) if np.isclose(y, val) ] - largeur_miroir_polygone = liste_points_miroir[-2] - liste_points_miroir[0] + largeur_miroir_polygone = liste_points_miroir[-2] - \ + liste_points_miroir[0] largeur_miroir += largeur_miroir_polygone polygone_shapely = ShapelyPolygon(points_polygone) diff --git a/src/View/Tools/Plot/PamhyrCanvas.py b/src/View/Tools/Plot/PamhyrCanvas.py index c4bbf4d3..ee1f5373 100644 --- a/src/View/Tools/Plot/PamhyrCanvas.py +++ b/src/View/Tools/Plot/PamhyrCanvas.py @@ -17,6 +17,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure + class MplCanvas(FigureCanvasQTAgg): def __init__(self, width=5, height=4, dpi=100): fig = Figure( diff --git a/src/View/Tools/Plot/PamhyrToolbar.py b/src/View/Tools/Plot/PamhyrToolbar.py index c88dfacf..7d769d32 100644 --- a/src/View/Tools/Plot/PamhyrToolbar.py +++ b/src/View/Tools/Plot/PamhyrToolbar.py @@ -39,7 +39,7 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): """ isometric_signal = pyqtSignal(str) - def __init__(self, canvas, parent, items = ["home", "move", "zoom", "save"]): + def __init__(self, canvas, parent, items=["home", "move", "zoom", "save"]): """PamhyrPlotToolbar Args: @@ -56,12 +56,15 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): icons = [] if "home" in items: - self.toolitems.append(('Home', _translate("Toolbar", 'Default view'), 'home', 'home')) + self.toolitems.append( + ('Home', _translate("Toolbar", 'Default view'), 'home', 'home')) self.toolitems.append((None, None, None, None)) if "back/forward" in items: - self.toolitems.append(('Back', _translate("Toolbar", 'Back to previous view'), 'back', 'back')) - self.toolitems.append(('Forward', _translate("Toolbar", 'Return to next view'), 'forward', 'forward')) + self.toolitems.append( + ('Back', _translate("Toolbar", 'Back to previous view'), 'back', 'back')) + self.toolitems.append(('Forward', _translate( + "Toolbar", 'Return to next view'), 'forward', 'forward')) self.toolitems.append((None, None, None, None)) if "move" in items: @@ -73,7 +76,8 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): self.toolitems.append((None, None, None, None)) if "zoom" in items: - self.toolitems.append(('Zoom', _translate("Toolbar", 'Zoom'), 'zoom_to_rect', 'zoom')) + self.toolitems.append( + ('Zoom', _translate("Toolbar", 'Zoom'), 'zoom_to_rect', 'zoom')) self.toolitems.append((None, None, None, None)) icon_zoom = QtGui.QIcon() @@ -81,7 +85,7 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): os.path.abspath(f"{file_path}/../ui/ressources/zoom.png") )) - icons.append(("zoom",icon_zoom)) + icons.append(("zoom", icon_zoom)) if "iso" in items: self.toolitems.append( @@ -173,10 +177,16 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): def toolitems_translation(self): self._actions['home'].setToolTip(_translate("Toolbar", "Default view")) - self._actions['back'].setToolTip(_translate("Toolbar", "Back to previous view")) - self._actions['forward'].setToolTip(_translate("Toolbar", "Return to next view")) - self._actions['pan'].setToolTip(_translate("Toolbar", "Axes panoramic")) + self._actions['back'].setToolTip( + _translate("Toolbar", "Back to previous view")) + self._actions['forward'].setToolTip( + _translate("Toolbar", "Return to next view")) + self._actions['pan'].setToolTip( + _translate("Toolbar", "Axes panoramic")) self._actions['zoom'].setToolTip(_translate("Toolbar", "Zoom")) - self._actions['save_figure'].setToolTip(_translate("Toolbar", "Save the figure")) - self.action_isometric_view.setToolTip(_translate("Toolbar", "Isometric view (Shift+W)")) - self.action_auto_global_view.setToolTip(_translate("Toolbar", "Auto scale view (Shift+X)")) + self._actions['save_figure'].setToolTip( + _translate("Toolbar", "Save the figure")) + self.action_isometric_view.setToolTip( + _translate("Toolbar", "Isometric view (Shift+W)")) + self.action_auto_global_view.setToolTip( + _translate("Toolbar", "Auto scale view (Shift+X)")) diff --git a/src/config.py b/src/config.py index b3ddf6b4..7477f57e 100644 --- a/src/config.py +++ b/src/config.py @@ -32,6 +32,7 @@ config_file = "config.sqlite3" logger = logging.getLogger() + class Config(SQL): def __init__(self): self._version = '0.0.3' @@ -40,15 +41,17 @@ class Config(SQL): logging.info(f"Configuration file : {self.filename}") - super(Config, self).__init__(filename = self.filename) + super(Config, self).__init__(filename=self.filename) def _create(self): # Info (meta data) - self.execute("CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") + self.execute( + "CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") self.execute(f"INSERT INTO info VALUES ('version', '{self._version}')") # Key / Value (data) - self.execute("CREATE TABLE data(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") + self.execute( + "CREATE TABLE data(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") # Solver self.execute(""" @@ -79,12 +82,15 @@ class Config(SQL): self.commit() - def _update(self): - version = self.execute(f"SELECT value FROM info WHERE key='version'")[0] + version = self.execute( + f"SELECT value FROM info WHERE key='version'")[0] if version != self._version: - logger.info(f"Configuration file update from {version} to {self._version}...") + logger.info( + "Configuration file update from " + + f"{version} to {self._version}..." + ) major, minor, release = version.strip().split(".") @@ -92,6 +98,8 @@ class Config(SQL): if int(release) < 2: # Add default solver posix = os.name == 'posix' + ext = "" if posix else ".exe" + self.execute(f""" INSERT INTO solver VALUES ( 'mage8', @@ -101,13 +109,14 @@ class Config(SQL): '', '', '', '', - '@install_dir/mage/mage{"" if posix else ".exe"} @args @input', + '@install_dir/mage/mage{ext} @args @input', '' ) """) if int(release) < 3: self.execute(f"INSERT INTO data VALUES ('last_study', '')") - self.execute(f"INSERT INTO data VALUES ('close_correctly', 'True')") + self.execute( + f"INSERT INTO data VALUES ('close_correctly', 'True')") self.commit() @@ -116,7 +125,7 @@ class Config(SQL): solvers = self.execute( "SELECT * FROM solver", - fetch_one = False + fetch_one=False ) for solver in solvers: solver_type = solver[0] @@ -139,7 +148,7 @@ class Config(SQL): id = 0 stricklers = self.execute( "SELECT * FROM stricklers", - fetch_one = False + fetch_one=False ) for strickler in stricklers: new = Stricklers() @@ -210,7 +219,7 @@ class Config(SQL): '{self._sql_format(solver._cmd_output)}' ) """, - commit = True) + commit=True) def _save_stricklers(self): self.execute(f"DELETE FROM stricklers") @@ -263,10 +272,12 @@ class Config(SQL): self._solvers = [] posix = os.name == 'posix' + ext = "" if posix else ".exe" + ctor = solver_type_list["mage8"] new = ctor("default-mage") new._description = "Default Pamhyr2 mage 8 version" - new._cmd_solver = f""""@install_dir/mage/mage{"" if posix else ".exe"}" @args @input""" + new._cmd_solver = f""""@install_dir/mage/mage{ext}" @args @input""" self._solvers.append(new) # Meshing tool @@ -300,14 +311,22 @@ class Config(SQL): def set_close_correctly(self): self.close_correctly = True - self.execute(f"UPDATE data SET value='True' WHERE key='close_correctly'") + self.execute( + f"UPDATE data SET value='True' WHERE key='close_correctly'") self.commit() def set_last_study(self, filename): self.last_study = filename self.close_correctly = False - self.execute(f"UPDATE data SET value='{self._sql_format(self.last_study)}' WHERE key='last_study'") - self.execute(f"UPDATE data SET value='{self.close_correctly}' WHERE key='close_correctly'") + self.execute( + "UPDATE data SET " + + f"value='{self._sql_format(self.last_study)}' " + + "WHERE key='last_study'" + ) + self.execute( + f"UPDATE data SET value='{self.close_correctly}' " + + "WHERE key='close_correctly'" + ) self.commit() @classmethod @@ -320,7 +339,7 @@ class Config(SQL): ndir = os.path.expanduser('~') + config_dir ndir = ndir.replace("/", "\\") - os.makedirs(ndir, exist_ok = True) + os.makedirs(ndir, exist_ok=True) file = ndir + config_file return file diff --git a/src/init.py b/src/init.py index a6eb3aa4..9a2305f4 100644 --- a/src/init.py +++ b/src/init.py @@ -16,7 +16,8 @@ # -*- coding: utf-8 -*- -import sys, os +import sys +import os import locale import logging @@ -53,13 +54,14 @@ try: handler.setFormatter(formatter) handler.setLevel(logging.DEBUG) logger.addHandler(handler) -except: - logger.error("Failed to create logfile...") +except Exception as e: + logger.error(f"Failed to create logfile... {e}") def legal_info(): "Display in stdout Pamhyr infomation about licence and version" - blue = lambda s: logger.info(f"{logger_color_blue()}{s}{logger_color_reset()}") + def blue(s): return logger.info( + f"{logger_color_blue()}{s}{logger_color_reset()}") blue("""`7MM\"""Mq. db `7MMM. ,MMF'`7MMF' `7MMF'`YMM' `MM'`7MM\"""Mq.""") blue(""" MM `MM. ;MM: MMMb dPMM MM MM VMA ,V MM `MM.""") @@ -76,11 +78,15 @@ def legal_info(): ) ), "r") as f: version = f.readline().strip() - logger.info(f"version: {logger_color_green()}{version}{logger_color_reset()}") + logger.info( + f"version: {logger_color_green()}{version}{logger_color_reset()}") logger.info("license: pamhyr Copyright (C) 2023 INRAE") logger.info("license: This program comes with ABSOLUTELY NO WARRANTY.") - logger.info("license: This is free software, and you are welcome to redistribute it") + logger.info( + "license: This is free software," + + " and you are welcome to redistribute it" + ) logger.info("license: under certain conditions.") @@ -90,9 +96,13 @@ def debug_info(): info = QLibraryInfo logger.debug(f"Qt PrefixPath: {info.location(QLibraryInfo.PrefixPath)}") - logger.debug(f"Qt LibExecPath: {info.location(QLibraryInfo.LibraryExecutablesPath)}") + logger.debug( + "Qt LibExecPath: " + + f"{info.location(QLibraryInfo.LibraryExecutablesPath)}" + ) -def setup_lang(app, conf:Config): + +def setup_lang(app, conf: Config): "Return QTranslator configured for current language" lang_file = "" translator = QTranslator() diff --git a/src/pamhyr.py b/src/pamhyr.py index 4ae6a7dc..2381da01 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -18,7 +18,8 @@ # -*- coding: utf-8 -*- -import sys, os +import sys +import os import locale import logging @@ -46,19 +47,24 @@ scripts = { "3DST": Script3DST, } + def usage(argv): logger.info("") logger.info(f"Usage: {argv[0]}