From 2ed3254b46556785f5dfdd4b81d677c3699e4c05 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 28 Jun 2023 16:50:08 +0200 Subject: [PATCH 001/289] Model: Start export model into SQLite database. * Study: Create table. * River: Create node and reach table. * Geometry: Create table only for profileXYZ and pointXYZ. --- src/Model/DB.py | 148 +++++++++++++++++++++++++++++++ src/Model/Except.py | 2 +- src/Model/Geometry/PointXYZ.py | 35 +++++++- src/Model/Geometry/ProfileXYZ.py | 37 +++++++- src/Model/Geometry/Reach.py | 36 +++++++- src/Model/Network/Node.py | 9 ++ src/Model/River.py | 102 ++++++++++++++++++++- src/Model/Serializable.py | 4 +- src/Model/Study.py | 89 +++++++++++++++---- src/View/MainWindow.py | 6 +- src/config.py | 2 +- src/tools.py | 9 +- 12 files changed, 450 insertions(+), 29 deletions(-) create mode 100644 src/Model/DB.py diff --git a/src/Model/DB.py b/src/Model/DB.py new file mode 100644 index 00000000..5206555a --- /dev/null +++ b/src/Model/DB.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +import os +import sqlite3 + +from pathlib import Path + +from tools import SQL +from Model.Except import NotImplementedMethodeError + +# Top level model class +class SQLModel(SQL): + _sub_classes = [] + + def _init_db_file(self, db, is_new = True): + exists = Path(db).exists() + + if exists and is_new: + os.remove(db) + + self._db = sqlite3.connect(db) + self._cur = self._db.cursor() + + if is_new: + self._create() # Create db + self._save() # Save + else: + self._update() # Update db scheme if necessary + self._load() # Load data + + def _create_submodel(self): + for cls in self._sub_classes: + requests = cls._sql_create( + lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ) + ) + + def _create(self): + raise NotImplementedMethodeError(self, self._create) + + def _update_submodel(self, version): + for cls in self._sub_classes: + requests = cls._sql_update( + lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ), + version + ) + + def _update(self): + raise NotImplementedMethodeError(self, self._update) + + def _save_submodel(self, objs): + for obj in objs: + requests = obj._sql_save( + lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ) + ) + + def _save(self): + raise NotImplementedMethodeError(self, self._save) + + @classmethod + def _load(cls, filename): + raise NotImplementedMethodeError(cls, cls._load) + +# Sub model class +class SQLSubModel(object): + _sub_classes = [] + + def _sql_format(self, value): + # Replace ''' by ''' to preserve SQL injection + if type(value) == str: + value = value.replace("'", "'") + return value + + @classmethod + def _create_submodel(cls, execute): + for sc in cls._sub_classes: + sc._sql_create(execute) + + @classmethod + def _sql_create(cls, execute): + """Create data base scheme + + Args: + execute: Function to exec SQL resquest + + Returns: + Return true, otherelse false if an issue appear + """ + raise NotImplementedMethodeError(cls, cls._sql_create) + + @classmethod + def _update_submodel(cls, execute, version): + for sc in cls._sub_classes: + sc._sql_update(execute, version) + + @classmethod + def _sql_update(cls, execute, version): + """Update data base scheme + + Args: + execute: Function to exec SQL resquest + version: Current database version + + Returns: + Return true, otherelse false if an issue appear + """ + raise NotImplementedMethodeError(cls, cls._sql_update) + + @classmethod + def _sql_load(cls, execute, data = None): + """Load instance of this class from SQL data base + + Args: + execute: Function to exec SQL request + data: Optional data for the class constructor + + Returns: + Return new instance of class + """ + raise NotImplementedMethodeError(cls, cls._sql_load) + + def _save_submodel(self, execute): + for sc in self._sub_classes: + sc._sql_update(execute) + + def _sql_save(self, execute, data = None): + """Save class data to data base + + Args: + execute: Function to exec SQL resquest + data: Optional additional information for save + + Returns: + Return true, otherelse false if an issue appear during + save + """ + raise NotImplementedMethodeError(self, self._sql_save) diff --git a/src/Model/Except.py b/src/Model/Except.py index 8480e3ee..ad311666 100644 --- a/src/Model/Except.py +++ b/src/Model/Except.py @@ -70,7 +70,7 @@ class NotImplementedMethodeError(ExeceptionWithMessageBox): f" '{self.func.__name__}' " + _translate("Exception", "not implemented") + _translate("Exception", "for class") + - f" '{self.obj.__class__}'" + f" '{self.obj.__class__ if self.obj.__class__ != type else self.obj}'" ) def header(self): diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 5f99f67b..9321d5bf 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -3,9 +3,12 @@ from math import dist import numpy as np +from Model.DB import SQLSubModel from Model.Geometry.Point import Point -class PointXYZ(Point): +class PointXYZ(Point, SQLSubModel): + _sub_classes = [] + def __init__(self, x:float = 0.0, y:float = 0.0, z:float = 0.0, name:str = "", status = None): super(PointXYZ, self).__init__(name=name, status=status) @@ -14,6 +17,36 @@ class PointXYZ(Point): self._y = float(y) self._z = float(z) + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE geometry_pointXYZ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT, + x INTEGER NOT NULL, + y INTEGER NOT NULL, + z INTEGER NOT NULL, + profile INTEGER NOT NULL, + FOREIGN KEY(profile) REFERENCES profileXYZ(id) + ) + """) + + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + cls._update_submodel(execute, version) + return True + + @classmethod + def _sql_load(cls, execute, data = None): + return None + + def _sql_save(self, execute, data = None): + return True + + @classmethod def from_data(cls, header, data): point = None diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 08cf2cf4..2013dea3 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -5,12 +5,17 @@ from typing import List from tools import timer +from Model.DB import SQLSubModel from Model.Except import ClipboardFormatError from Model.Geometry.Profile import Profile from Model.Geometry.PointXYZ import PointXYZ from Model.Geometry.Vector_1d import Vector1d -class ProfileXYZ(Profile): +class ProfileXYZ(Profile, SQLSubModel): + _sub_classes = [ + PointXYZ, + ] + def __init__(self, name: str = "", kp: float = 0., @@ -41,6 +46,36 @@ class ProfileXYZ(Profile): status = status, ) + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE geometry_profileXYZ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT, + reach INTEGER NOT NULL, + kp REAL NOT NULL, + num INTEGER NOT NULL, + code1 INTEGER NOT NULL, + code2 INTEGER NOT NULL, + FOREIGN KEY(reach) REFERENCES river_reach(id) + ) + """) + + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + cls._update_submodel(execute, version) + return True + + @classmethod + def _sql_load(cls, execute, data = None): + return None + + def _sql_save(self, execute, data = None): + return True + @classmethod def from_data(cls, header, data): profile = None diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index ebff2dc7..c5415c76 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -10,12 +10,18 @@ from functools import reduce from tools import flatten, timer, trace +from Model.DB import SQLSubModel + from Model.Geometry.Profile import Profile from Model.Geometry.ProfileXYZ import ProfileXYZ from Model.Except import FileFormatError, exception_message_box -class Reach: +class Reach(SQLSubModel): + _sub_classes = [ + ProfileXYZ, + ] + def __init__(self, status=None, parent=None): self._status = status self._parent = parent @@ -24,6 +30,34 @@ class Reach: self._guidelines_is_valid = False self._guidelines = {} + @classmethod + def _sql_create(cls, execute): + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + cls._update_submodel(execute, version) + return None + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls(status = data["status"], parent = data["parent"]) + + new._profiles = ProfileXYZ._sql_load( + execute, + data = { + "status": data["status"], + "reach": new, + } + ) + + return new + + def _sql_save(self, execute, data = None): + cls._save_submodel(execute, data) + return True + def profile(self, i): """Returns profile at index i diff --git a/src/Model/Network/Node.py b/src/Model/Network/Node.py index 5da21909..5281a236 100644 --- a/src/Model/Network/Node.py +++ b/src/Model/Network/Node.py @@ -38,6 +38,15 @@ class Node(object): def name(self): return self._name + @property + def x(self): + return self.pos.x + + @property + 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/River.py b/src/Model/River.py index 580d2b8f..63137639 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from Model.DB import SQLSubModel + from Model.Network.Node import Node from Model.Network.Edge import Edge from Model.Network.Graph import Graph @@ -16,7 +18,9 @@ from Model.SolverParameters.SolverParametersList import SolverParametersList from Solver.Solvers import solver_type_list -class RiverNode(Node): +class RiverNode(Node, SQLSubModel): + _sub_classes = [] + def __init__(self, id:str, name:str, x:float, y:float, status = None): @@ -28,6 +32,31 @@ class RiverNode(Node): self._locker = None + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE river_node( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + x REAL NOT NULL, + y REAL NOT NULL + ) + """) + + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + return None + + @classmethod + def _sql_load(cls, execute, data = None): + return True + + def _sql_save(self, execute, data = None): + return True + @property def locker(self): return self._locker @@ -37,7 +66,12 @@ class RiverNode(Node): self._locker = locker -class RiverReach(Edge): +class RiverReach(Edge, SQLSubModel): + _sub_classes = [ + Reach, + # SectionList, + ] + def __init__(self, id:str, name:str, node1:RiverNode = None, node2:RiverNode = None, @@ -51,6 +85,33 @@ class RiverReach(Edge): self._reach = Reach(status=self._status, parent=self) self._sections = SectionList(status=self._status) + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE river_reach( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + node1 INTEGER, + node2 INTEGER, + FOREIGN KEY(node1) REFERENCES river_node(id), + FOREIGN KEY(node2) REFERENCES river_node(id) + ) + """) + + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + return None + + def _sql_save(self, execute, data = None): + return True + @property def reach(self): return self._reach @@ -59,7 +120,17 @@ class RiverReach(Edge): def sections(self): return self._sections -class River(Graph): +class River(Graph, SQLSubModel): + _sub_classes = [ + RiverNode, + RiverReach, + # BoundaryConditionList, + # LateralContributionList, + # InitialConditionsDict, + # StricklersList, + # SolverParametersList, + ] + def __init__(self, status=None): super(River, self).__init__(status=status) @@ -74,6 +145,31 @@ class River(Graph): self._stricklers = StricklersList(status=self._status) self._parameters = {} + @classmethod + def _sql_create(cls, execute): + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + return None + + def _sql_save(self, execute, data = None): + return True + + @property + def reach(self): + return self._reach + + @property + def sections(self): + return self._sections + + @property def boundary_condition(self): return self._boundary_condition diff --git a/src/Model/Serializable.py b/src/Model/Serializable.py index 17b12345..57f2a4c1 100644 --- a/src/Model/Serializable.py +++ b/src/Model/Serializable.py @@ -3,8 +3,8 @@ import pickle class Serializable(): - def __init__(self, filename): - self.filename = filename + def __init__(self): + return @classmethod def open(cls, filename): diff --git a/src/Model/Study.py b/src/Model/Study.py index a07d32c4..238da3b4 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -3,18 +3,28 @@ import os from datetime import datetime +from Model.DB import SQLModel from Model.Saved import SavedStatus from Model.Serializable import Serializable - +from Model.Except import NotImplementedMethodeError from Model.River import River from Checker.Study import * -class Study(Serializable): - def __init__(self): - # Serialization information - super(Study, self).__init__("") - self.filename = "" +class Study(SQLModel): + _sub_classes = [ + River, + ] + + def __init__(self, filename = None, init_new = True): + # Metadata + self._version = "0.0.0" + self.creation_date = datetime.now() + self.last_modification_date = datetime.now() + self.last_save_date = datetime.now() + + self._filename = filename + super(Study, self).__init__(filename = filename) self.status = SavedStatus() @@ -25,12 +35,9 @@ class Study(Serializable): self._time_system = "time" self._date = datetime.fromtimestamp(0) - self.creation_date = datetime.now() - self.last_modification_date = datetime.now() - self.last_save_date = datetime.now() - - # Study data - self._river = River(status = self.status) + if init_new: + # Study data + self._river = River(status = self.status) @classmethod def checkers(cls): @@ -66,6 +73,15 @@ class Study(Serializable): self._name = str(name) self.status.modified() + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, filename): + self._filename = str(filename) + self._init_db_file(filename, is_new = True) + @property def time_system(self): return self._time_system @@ -88,9 +104,9 @@ class Study(Serializable): self._date = timestamp self.status.modified() - @classmethod - def new(cls): - return cls() + # @classmethod + # def new(cls): + # return cls() @classmethod def new(cls, name, description, date = None): @@ -101,3 +117,46 @@ class Study(Serializable): me.use_date() me.date = date return me + + @classmethod + def open(cls, filename): + me = cls._load(filename) + return me + + ####### + # SQL # + ####### + + def _create(self): + # Info (metadata) + 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 + ) + + self._create_submodel() + self.commit() + + def _update(self): + version = self.execute(f"SELECT value FROM info WHERE key='version'") + + print(f"{version} == {self._version}") + if version == self._version: + return True + + print("TODO: update") + raise NotImplementedMethodeError(self, self._update) + + @classmethod + def _load(cls, filename): + new = cls(init_new = False) + + # Load river data + self._river = River.load() + + return new + + def _save(self): + + self.commit() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index ea23101a..340e3cf0 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -213,7 +213,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model.filename == "": + if self.model.filename is None or self.model.filename == "": file_name, _ = QFileDialog.getSaveFileName( self, "Save File", "", "Pamhyr(*.pkl)" @@ -282,9 +282,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self.model is None: dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.FileMode.ExistingFile) - dialog.setDefaultSuffix(".pkl") + dialog.setDefaultSuffix(".pamhyr") #dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden) - dialog.setNameFilters(['PamHyr (*.pkl)']) + dialog.setNameFilters(['PamHyr (*.pamhyr)']) if dialog.exec_(): file_name = dialog.selectedFiles() diff --git a/src/config.py b/src/config.py index 61c52272..64f5664a 100644 --- a/src/config.py +++ b/src/config.py @@ -19,7 +19,7 @@ class Config(SQL): self.filename = Config.filename() self.set_default_value() - super(Config, self).__init__(db = self.filename) + super(Config, self).__init__(filename = self.filename) def _create(self): # Info (meta data) diff --git a/src/tools.py b/src/tools.py index a3f20d04..17d24b4e 100644 --- a/src/tools.py +++ b/src/tools.py @@ -165,7 +165,7 @@ def old_pamhyr_date_to_timestamp(date:str): # from sqlite3. class SQL(object): - def __init__(self, db = "db.sqlite3"): + def _init_db_file(self, db): exists = Path(db).exists() self._db = sqlite3.connect(db) @@ -178,6 +178,13 @@ class SQL(object): self._update() # Update db scheme if necessary self._load() # Load data + + def __init__(self, filename = None): + self._db = None + + if filename is not None: + self._init_db_file(filename) + def commit(self): self._db.commit() From 4978f20e29befeb8545f4bf0c7a33763d318a715 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Thu, 29 Jun 2023 11:33:59 +0200 Subject: [PATCH 002/289] Network: Export model network to sqlite db. --- src/Model/DB.py | 71 ++++++++++++++++++++++---------------- src/Model/Network/Edge.py | 11 ++++-- src/Model/Network/Graph.py | 11 ++---- src/Model/Network/Node.py | 31 +++++++++++------ src/Model/River.py | 68 +++++++++++++++++++++++++++++++++--- src/Model/Study.py | 28 +++++++++++---- src/View/MainWindow.py | 12 +++---- 7 files changed, 167 insertions(+), 65 deletions(-) diff --git a/src/Model/DB.py b/src/Model/DB.py index 5206555a..013f688e 100644 --- a/src/Model/DB.py +++ b/src/Model/DB.py @@ -22,54 +22,65 @@ class SQLModel(SQL): self._cur = self._db.cursor() if is_new: + print("CREATE") self._create() # Create db - self._save() # Save + # self._save() # Save else: + print("UPDATE") self._update() # Update db scheme if necessary - self._load() # Load data + # self._load() # Load data + + + def __init__(self, filename = None): + self._db = None def _create_submodel(self): + fn = lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ) + for cls in self._sub_classes: - requests = cls._sql_create( - lambda sql: self.execute( - sql, - fetch_one = False, - commit = True - ) - ) + requests = cls._sql_create(fn) def _create(self): raise NotImplementedMethodeError(self, self._create) def _update_submodel(self, version): + fn = lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ) + + ok = True for cls in self._sub_classes: - requests = cls._sql_update( - lambda sql: self.execute( - sql, - fetch_one = False, - commit = True - ), - version - ) + ok &= cls._sql_update(fn, version) + + return ok def _update(self): raise NotImplementedMethodeError(self, self._update) - def _save_submodel(self, objs): + def _save_submodel(self, objs, data = None): + fn = lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ) + + ok = True for obj in objs: - requests = obj._sql_save( - lambda sql: self.execute( - sql, - fetch_one = False, - commit = True - ) - ) + ok &= obj._sql_save(fn) + + return ok def _save(self): raise NotImplementedMethodeError(self, self._save) @classmethod - def _load(cls, filename): + def _load(cls, filename = None): raise NotImplementedMethodeError(cls, cls._load) # Sub model class @@ -80,6 +91,8 @@ class SQLSubModel(object): # Replace ''' by ''' to preserve SQL injection if type(value) == str: value = value.replace("'", "'") + elif type(value) == bool: + value = 'TRUE' if value else 'FALSE' return value @classmethod @@ -130,9 +143,9 @@ class SQLSubModel(object): """ raise NotImplementedMethodeError(cls, cls._sql_load) - def _save_submodel(self, execute): - for sc in self._sub_classes: - sc._sql_update(execute) + def _save_submodel(self, execute, objs, data = None): + for o in objs: + o._sql_save(execute, data = data) def _sql_save(self, execute, data = None): """Save class data to data base diff --git a/src/Model/Network/Edge.py b/src/Model/Network/Edge.py index b0918a16..f1ced151 100644 --- a/src/Model/Network/Edge.py +++ b/src/Model/Network/Edge.py @@ -3,7 +3,9 @@ from Model.Network.Node import Node class Edge(object): - def __init__(self, id:str, name:str, + _id_cnt = 0 + + def __init__(self, id:int, name:str, node1:Node = None, node2:Node = None, status = None): @@ -11,7 +13,12 @@ class Edge(object): self._status = status - self.id = id + if id == -1: + type(self)._id_cnt += 1 + self.id = type(self)._id_cnt + else: + self.id = id + self._name = name self.node1 = node1 diff --git a/src/Model/Network/Graph.py b/src/Model/Network/Graph.py index eb91614a..0f432495 100644 --- a/src/Model/Network/Graph.py +++ b/src/Model/Network/Graph.py @@ -14,9 +14,6 @@ class Graph(object): self._node_ctor = Node self._edge_ctor = Edge - self._nodes_ids = 0 - self._edges_ids = 0 - self._nodes = [] self._edges = [] @@ -83,12 +80,11 @@ class Graph(object): def _create_node(self, x:float, y:float): node = self._node_ctor( - self._nodes_ids, - f"Node {self._nodes_ids}", + -1, + "", x = x, y = y, status = self._status ) - self._nodes_ids += 1 return node def _add_node(self, node): @@ -118,11 +114,10 @@ class Graph(object): def _create_edge(self, n1:Node, n2:Node): edge = self._edge_ctor( - self._edges_ids, + -1, "", n1, n2, status = self._status ) - self._edges_ids += 1 return edge diff --git a/src/Model/Network/Node.py b/src/Model/Network/Node.py index 5281a236..5f37101f 100644 --- a/src/Model/Network/Node.py +++ b/src/Model/Network/Node.py @@ -3,33 +3,44 @@ from Model.Network.Point import Point class Node(object): - def __init__(self, id:str, name:str, + _id_cnt = 0 + + def __init__(self, id:int, name:str, x:float = 0.0, y:float = 0.0, status = None): super(Node, self).__init__() self._status = status - self.id = id - self._name = name + if id == -1: + type(self)._id_cnt += 1 + self.id = type(self)._id_cnt + else: + self.id = id + + if name == "": + self._name = f"Node {self.id}" + else: + self._name = name + self.pos = Point(x, y) - def __getitem__(self, name): + def __getitem__(self, key): ret = None - if name == "name": + if key == "name": ret = self._name - elif name == "id": + elif key == "id": ret = self.id - elif name == "pos": + elif key == "pos": ret = f"({self.pos.x},{self.pos.y})" return ret - def __setitem__(self, name, value): - if name == "name": + def __setitem__(self, key, value): + if key == "name": self._name = value - elif name == "id": + elif key == "id": self.id = value self._status.modified() diff --git a/src/Model/River.py b/src/Model/River.py index 63137639..09ce95c1 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -52,9 +52,26 @@ class RiverNode(Node, SQLSubModel): @classmethod def _sql_load(cls, execute, data = None): - return True + nodes = [] + + table = execute("SELECT id, name, x, y FROM river_node") + for row in table: + # Update id counter + cls._id_cnt = max(cls._id_cnt, row[0]) + # Create new node + nodes.append(cls(*row, **data)) + + return nodes 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}"+ + ")" + ) + execute(sql) + return True @property @@ -91,6 +108,7 @@ class RiverReach(Edge, SQLSubModel): CREATE TABLE river_reach( id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, + enable BOOLEAN NOT NULL, node1 INTEGER, node2 INTEGER, FOREIGN KEY(node1) REFERENCES river_node(id), @@ -107,9 +125,38 @@ class RiverReach(Edge, SQLSubModel): @classmethod def _sql_load(cls, execute, data = None): - return None + reachs = [] + + 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]) + # Create new reach + id = row[0] + name = row[1] + enable = (row[2] == 1) + # Get nodes corresponding to db foreign key id + 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) + reachs.append(new) + + return reachs def _sql_save(self, execute, data = None): + sql = ( + "INSERT OR REPLACE INTO " + + "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}"+ + ")" + ) + execute(sql) + return True @property @@ -152,13 +199,27 @@ class River(Graph, SQLSubModel): @classmethod def _sql_update(cls, execute, version): + cls._update_submodel(execute, version) return True @classmethod def _sql_load(cls, execute, data = None): - return None + new = cls(data["status"]) + new._nodes = RiverNode._sql_load( + execute, + data + ) + data["nodes"] = new.nodes() + new._edges = RiverReach._sql_load( + execute, + data + ) + + return new def _sql_save(self, execute, data = None): + objs = self._nodes + self._edges + self._save_submodel(execute, objs, data) return True @property @@ -169,7 +230,6 @@ class River(Graph, SQLSubModel): def sections(self): return self._sections - @property def boundary_condition(self): return self._boundary_condition diff --git a/src/Model/Study.py b/src/Model/Study.py index 238da3b4..50edc5ce 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -38,6 +38,8 @@ class Study(SQLModel): if init_new: # Study data self._river = River(status = self.status) + else: + self._init_db_file(filename, is_new = False) @classmethod def checkers(cls): @@ -141,22 +143,36 @@ class Study(SQLModel): def _update(self): version = self.execute(f"SELECT value FROM info WHERE key='version'") - print(f"{version} == {self._version}") - if version == self._version: + print(f"{version[0]} == {self._version}") + if version[0] == self._version: return True - print("TODO: update") + if self._update_submodel(version): + self.execute(f"UPDATE info SET value='{self._version}' WHERE key='version'") + return True + + print("TODO: update failed") raise NotImplementedMethodeError(self, self._update) @classmethod def _load(cls, filename): - new = cls(init_new = False) + new = cls(init_new = False, filename = filename) + + # TODO: Load metadata + print("TODO: Load metadata") # Load river data - self._river = River.load() + new._river = River._sql_load( + lambda sql: new.execute( + sql, + fetch_one = False, + commit = True + ), + data = {"status": new.status} + ) return new def _save(self): - + self._save_submodel([self._river]) self.commit() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 340e3cf0..49387884 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -216,13 +216,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self.model.filename is None or self.model.filename == "": file_name, _ = QFileDialog.getSaveFileName( self, "Save File", - "", "Pamhyr(*.pkl)" + "", "Pamhyr(*.pamhyr)" ) - if file_name[-4:] == ".pkl": + if file_name[-4:] == ".pamhyr": self.model.filename = file_name else: - self.model.filename = file_name + ".pkl" + self.model.filename = file_name + ".pamhyr" self.model.save() @@ -237,13 +237,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): """ file_name, _ = QFileDialog.getSaveFileName( self, "Save File", - "", "Pamhyr(*.pkl)" + "", "Pamhyr(*.pamhyr)" ) - if file_name[-4:] == ".pkl": + if file_name[-4:] == ".pamhyr": self.model.filename = file_name else: - self.model.filename = file_name + ".pkl" + self.model.filename = file_name + ".pamhyr" self.model.save() From 36f792d432c1f90c79dde46abeb210ad93fca5da Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Thu, 29 Jun 2023 15:39:35 +0200 Subject: [PATCH 003/289] Model, BC: Add SQL export for boundary condition. --- .../BoundaryCondition/BoundaryCondition.py | 138 +++++++++++++++++- .../BoundaryConditionList.py | 42 +++++- .../BoundaryConditionTypes.py | 20 +-- src/Model/River.py | 17 ++- src/tools.py | 1 + 5 files changed, 202 insertions(+), 16 deletions(-) diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index cfb33fca..e88a4f8f 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -2,15 +2,24 @@ from tools import trace, timer, old_pamhyr_date_to_timestamp +from Model.DB import SQLSubModel from Model.Except import NotImplementedMethodeError -class BoundaryCondition(object): - def __init__(self, name:str = "", +class BoundaryCondition(SQLSubModel): + _sub_classes = [] + _id_cnt = 0 + + def __init__(self, id:int = -1, name:str = "", status = None): super(BoundaryCondition, self).__init__() self._status = status + if id == -1: + self.id = type(self)._id_cnt + else: + self.id = id + self._name = name self._type = "" self._node = None @@ -18,6 +27,131 @@ class BoundaryCondition(object): self._header = [] self._types = [float, float] + type(self)._id_cnt = max(type(self)._id_cnt + 1, self.id) + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE boundary_condition( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, + tab TEXT NOT NULL, + node INTEGER, + FOREIGN KEY(node) REFERENCES river_node(id) + ) + """) + + execute(""" + CREATE TABLE boundary_condition_data( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ind INTEGER NOT NULL, + data0 TEXT NOT NULL, + data1 TEXT NOT NULL, + bc INTEGER, + FOREIGN KEY(bc) REFERENCES boundary_condition(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _get_ctor_from_type(cls, t): + from Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, PonctualContribution, + TimeOverZ, TimeOverDischarge, ZOverDischarge + ) + + res = NotDefined + if t == "PC": + res = PonctualContribution + elif t == "TZ": + res = TimeOverZ + elif t == "TD": + res = TimeOverDischarge + elif t == "ZD": + res = ZOverDischarge + return res + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + tab = data["tab"] + + table = execute( + "SELECT id, name, type, node " + + "FROM boundary_condition " + + f"WHERE tab = '{tab}'" + ) + + for row in table: + t = row[2] + ctor = cls._get_ctor_from_type(t) + bc = ctor( + id = row[0], + name = row[1], + status = data['status'] + ) + bc.node = next(filter(lambda n: n.id == row[3], data["nodes"])) + + values = execute( + "SELECT ind, data0, data1 FROM boundary_condition_data " + + f"WHERE bc = '{bc.id}'" + ) + # Create dummy data list + for _ in values: + bc.add(0) + # Write data + for v in values: + ind = v[0] + data0 = bc._types[0](v[1]) + data1 = bc._types[1](v[2]) + # Replace data at pos ind + bc._data[ind] = (data0, data1) + + new.append(bc) + + return new + + def _sql_save(self, execute, data = None): + tab = data["tab"] + + execute(f"DELETE FROM boundary_condition WHERE id = {self.id}") + execute(f"DELETE FROM boundary_condition_data WHERE bc = {self.id}") + + node = -1 + if self._node is not None: + node = self._node.id + + sql = ( + "INSERT INTO " + + "boundary_condition(id, name, type, tab, node) "+ + "VALUES (" + + f"{self.id}, '{self._sql_format(self._name)}', " + + f"'{self._sql_format(self._type)}', '{tab}', {node}" + + ")" + ) + execute(sql) + + ind = 0 + for d in self._data: + data0 = self._sql_format(str(d[0])) + data1 = self._sql_format(str(d[1])) + + sql = ( + "INSERT INTO " + + "boundary_condition_data(ind, data0, data1, bc) "+ + f"VALUES ({ind}, '{data0}', {data1}, {self.id})" + ) + execute(sql) + ind += 1 + + return True + def __len__(self): return len(self._data) diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py index 5f2857da..c59f97b0 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionList.py +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -3,15 +3,21 @@ from copy import copy from tools import trace, timer +from Model.DB import SQLSubModel from Model.Except import NotImplementedMethodeError +from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, TimeOverZ, TimeOverDischarge, ZOverDischarge ) -class BoundaryConditionList(object): +class BoundaryConditionList(SQLSubModel): + _sub_classes = [ + BoundaryCondition, + ] + def __init__(self, status = None): super(BoundaryConditionList, self).__init__() @@ -23,6 +29,40 @@ class BoundaryConditionList(object): "suspenssion" : [] } + @classmethod + def _sql_create(cls, execute): + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls(status = data['status']) + + if data is None: + data = {} + + for tab in new._tabs: + data["tab"] = tab + new._tabs[tab] = BoundaryCondition._sql_load( + execute, data + ) + + return new + + def _sql_save(self, execute, data = None): + if data is None: + data = {} + + for tab in self._tabs: + data["tab"] = tab + for bc in self._tabs[tab]: + bc._sql_save(execute, data = data) + + return True + def len(self, lst): return len(self._tabs[lst]) diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py index 98a48843..c87e5b83 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -6,8 +6,8 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition class NotDefined(BoundaryCondition): - def __init__(self, name:str = "", status = None): - super(NotDefined, self).__init__(name=name, status=status) + def __init__(self, id:int = -1, name:str = "", status = None): + super(NotDefined, self).__init__(id=id, name=name, status=status) self._type = "ND" self._header = ["x", "y"] @@ -17,8 +17,8 @@ class NotDefined(BoundaryCondition): return 0.0 class PonctualContribution(BoundaryCondition): - def __init__(self, name:str = "", status = None): - super(PonctualContribution, self).__init__(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"] @@ -29,8 +29,8 @@ class PonctualContribution(BoundaryCondition): return ["liquid"] class TimeOverZ(BoundaryCondition): - def __init__(self, name:str = "", status = None): - super(TimeOverZ, self).__init__(name=name, status=status) + def __init__(self, id:int = -1, name:str = "", status = None): + super(TimeOverZ, self).__init__(id=id, name=name, status=status) self._type = "TZ" self._header = ["time", "z"] @@ -41,8 +41,8 @@ class TimeOverZ(BoundaryCondition): return ["liquid"] class TimeOverDischarge(BoundaryCondition): - def __init__(self, name:str = "", status = None): - super(TimeOverDischarge, self).__init__(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"] @@ -53,8 +53,8 @@ class TimeOverDischarge(BoundaryCondition): return ["liquid"] class ZOverDischarge(BoundaryCondition): - def __init__(self, name:str = "", status = None): - super(ZOverDischarge, self).__init__(name=name, status=status) + def __init__(self, id:int = -1, name:str = "", status = None): + super(ZOverDischarge, self).__init__(id=id, name=name, status=status) self._type = "ZD" self._header = ["z", "discharge"] diff --git a/src/Model/River.py b/src/Model/River.py index 09ce95c1..5316f892 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -48,7 +48,7 @@ class RiverNode(Node, SQLSubModel): @classmethod def _sql_update(cls, execute, version): - return None + return True @classmethod def _sql_load(cls, execute, data = None): @@ -171,7 +171,7 @@ class River(Graph, SQLSubModel): _sub_classes = [ RiverNode, RiverReach, - # BoundaryConditionList, + BoundaryConditionList, # LateralContributionList, # InitialConditionsDict, # StricklersList, @@ -204,21 +204,32 @@ class River(Graph, SQLSubModel): @classmethod def _sql_load(cls, execute, data = None): + # Network new = cls(data["status"]) new._nodes = RiverNode._sql_load( execute, data ) data["nodes"] = new.nodes() + new._edges = RiverReach._sql_load( execute, data ) + data["edges"] = new.edges() + + # Boundary Condition + new._boundary_condition = BoundaryConditionList._sql_load( + execute, + data + ) return new def _sql_save(self, execute, data = None): - objs = self._nodes + self._edges + objs = (self._nodes + self._edges) + objs.append(self._boundary_condition) + self._save_submodel(execute, objs, data) return True diff --git a/src/tools.py b/src/tools.py index 17d24b4e..b54616d1 100644 --- a/src/tools.py +++ b/src/tools.py @@ -232,6 +232,7 @@ class SQL(object): return value def execute(self, cmd, fetch_one = True, commit = False): + print(f"[SQL] {cmd}") res = self._cur.execute(cmd) if commit: From a4acbdce16b7020960a016f83aa4ec2800c43109 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Thu, 29 Jun 2023 16:52:17 +0200 Subject: [PATCH 004/289] Model, LC: Add SQL export for lateral contribution. --- .../LateralContribution.py | 140 +++++++++++++++++- .../LateralContributionList.py | 42 +++++- .../LateralContributionTypes.py | 16 +- src/Model/River.py | 7 +- 4 files changed, 193 insertions(+), 12 deletions(-) diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index b6e16998..22d4ca2f 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -2,14 +2,24 @@ from tools import trace, timer, old_pamhyr_date_to_timestamp +from Model.DB import SQLSubModel from Model.Except import NotImplementedMethodeError -class LateralContribution(object): - def __init__(self, name:str = "", status = None): +class LateralContribution(SQLSubModel): + _sub_classes = [] + _id_cnt = 0 + + def __init__(self, id:int = -1, name:str = "", status = None): super(LateralContribution, self).__init__() self._status = status + if id == -1: + self.id = type(self)._id_cnt + else: + self.id = id + + self._name = name self._type = "" self._edge = None @@ -19,6 +29,132 @@ class LateralContribution(object): self._header = [] self._types = [float, float] + type(self)._id_cnt = max(type(self)._id_cnt + 1, self.id) + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE lateral_contribution( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, + tab TEXT NOT NULL, + edge INTEGER, + begin_kp REAL NOT NULL, + end_kp REAL NOT NULL, + FOREIGN KEY(edge) REFERENCES river_reach(id) + ) + """) + + execute(""" + CREATE TABLE lateral_contribution_data( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ind INTEGER NOT NULL, + data0 TEXT NOT NULL, + data1 TEXT NOT NULL, + lc INTEGER, + FOREIGN KEY(lc) REFERENCES lateral_contribution(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _get_ctor_from_type(cls, t): + from Model.LateralContribution.LateralContributionTypes import ( + NotDefined, LateralContrib, Rain, Evaporation + ) + + res = NotDefined + if t == "LC": + res = LateralContrib + elif t == "RA": + res = Rain + elif t == "EV": + res = Evaporation + return res + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + tab = data["tab"] + + table = execute( + "SELECT id, name, type, edge, begin_kp, end_kp " + + f"FROM lateral_contribution WHERE tab = '{tab}'" + ) + + for row in table: + t = row[2] + ctor = cls._get_ctor_from_type(t) + lc = ctor( + id = row[0], + name = row[1], + status = data['status'] + ) + lc.edge = next(filter(lambda e: e.id == row[3], data["edges"])) + lc._begin_kp = row[4] + lc._end_kp = row[5] + + values = execute( + "SELECT ind, data0, data1 FROM lateral_contribution_data " + + f"WHERE lc = '{lc.id}'" + ) + # Create dummy data list + for _ in values: + lc.add(0) + # Write data + for v in values: + ind = v[0] + data0 = lc._types[0](v[1]) + data1 = lc._types[1](v[2]) + # Replace data at pos ind + lc._data[ind] = (data0, data1) + + new.append(lc) + + return new + + def _sql_save(self, execute, data = None): + tab = data["tab"] + + execute(f"DELETE FROM lateral_contribution WHERE id = {self.id}") + execute(f"DELETE FROM lateral_contribution_data WHERE lc = {self.id}") + + edge = -1 + if self._edge is not None: + edge = self._edge.id + + sql = ( + "INSERT INTO " + + "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}, " + + f"{self._begin_kp}, {self._end_kp}" + + ")" + ) + execute(sql) + + ind = 0 + for d in self._data: + data0 = self._sql_format(str(d[0])) + data1 = self._sql_format(str(d[1])) + + sql = ( + "INSERT INTO " + + "lateral_contribution_data(ind, data0, data1, lc) "+ + f"VALUES ({ind}, '{data0}', {data1}, {self.id})" + ) + execute(sql) + ind += 1 + + return True + def __len__(self): return len(self._data) diff --git a/src/Model/LateralContribution/LateralContributionList.py b/src/Model/LateralContribution/LateralContributionList.py index c14d6f63..810a3188 100644 --- a/src/Model/LateralContribution/LateralContributionList.py +++ b/src/Model/LateralContribution/LateralContributionList.py @@ -3,13 +3,19 @@ from copy import copy from tools import trace, timer +from Model.DB import SQLSubModel from Model.Except import NotImplementedMethodeError +from Model.LateralContribution.LateralContribution import LateralContribution from Model.LateralContribution.LateralContributionTypes import ( NotDefined, LateralContrib, Rain, Evaporation, ) -class LateralContributionList(object): +class LateralContributionList(SQLSubModel): + _sub_classes = [ + LateralContribution, + ] + def __init__(self, status = None): super(LateralContributionList, self).__init__() @@ -21,6 +27,40 @@ class LateralContributionList(object): "suspenssion" : [] } + @classmethod + def _sql_create(cls, execute): + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls(status = data['status']) + + if data is None: + data = {} + + for tab in new._tabs: + data["tab"] = tab + new._tabs[tab] = LateralContribution._sql_load( + execute, data + ) + + return new + + def _sql_save(self, execute, data = None): + if data is None: + data = {} + + for tab in self._tabs: + data["tab"] = tab + for lc in self._tabs[tab]: + lc._sql_save(execute, data = data) + + return True + def len(self, lst): return len(self._tabs[lst]) diff --git a/src/Model/LateralContribution/LateralContributionTypes.py b/src/Model/LateralContribution/LateralContributionTypes.py index 7a3a8548..69d9946c 100644 --- a/src/Model/LateralContribution/LateralContributionTypes.py +++ b/src/Model/LateralContribution/LateralContributionTypes.py @@ -6,8 +6,8 @@ from Model.LateralContribution.LateralContribution import LateralContribution class NotDefined(LateralContribution): - def __init__(self, name:str = "", status=None): - super(NotDefined, self).__init__(name=name, status=status) + def __init__(self, id:int = -1, name:str = "", status=None): + super(NotDefined, self).__init__(id=id, name=name, status=status) self._type = "ND" self._header = ["x", "y"] @@ -17,8 +17,8 @@ class NotDefined(LateralContribution): return 0.0 class LateralContrib(LateralContribution): - def __init__(self, name:str = "", status=None): - super(LateralContrib, self).__init__(name=name, status=status) + def __init__(self, id:int = -1, name:str = "", status=None): + super(LateralContrib, self).__init__(id=id, name=name, status=status) self._type = "LC" self._header = ["time", "discharge"] @@ -29,8 +29,8 @@ class LateralContrib(LateralContribution): return ["liquid"] class Rain(LateralContribution): - def __init__(self, name:str = "", status=None): - super(Rain, self).__init__(name=name, status=status) + def __init__(self, id:int = -1, name:str = "", status=None): + super(Rain, self).__init__(id=id, name=name, status=status) self._type = "RA" self._header = ["time", "discharge"] @@ -41,8 +41,8 @@ class Rain(LateralContribution): return ["liquid"] class Evaporation(LateralContribution): - def __init__(self, name:str = "", status=None): - super(Evaporation, self).__init__(name=name, status=status) + def __init__(self, id:int = -1, name:str = "", status=None): + super(Evaporation, self).__init__(id=id, name=name, status=status) self._type = "EV" self._header = ["time", "discharge"] diff --git a/src/Model/River.py b/src/Model/River.py index 5316f892..452773c3 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -172,7 +172,7 @@ class River(Graph, SQLSubModel): RiverNode, RiverReach, BoundaryConditionList, - # LateralContributionList, + LateralContributionList, # InitialConditionsDict, # StricklersList, # SolverParametersList, @@ -223,12 +223,17 @@ class River(Graph, SQLSubModel): execute, data ) + new._lateral_contribution = LateralContributionList._sql_load( + execute, + data + ) return new def _sql_save(self, execute, data = None): objs = (self._nodes + self._edges) objs.append(self._boundary_condition) + objs.append(self._lateral_contribution) self._save_submodel(execute, objs, data) return True From 1921889309186a9b18075373de4068f9d6027a1b Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 30 Jun 2023 10:38:55 +0200 Subject: [PATCH 005/289] Model, Geometry: Save and load geometry into SQLite. --- src/Model/DB.py | 11 ++++-- src/Model/Geometry/PointXYZ.py | 50 +++++++++++++++++++++++- src/Model/Geometry/Profile.py | 12 +++++- src/Model/Geometry/ProfileXYZ.py | 66 +++++++++++++++++++++++++++++--- src/Model/Geometry/Reach.py | 11 +++--- src/Model/River.py | 11 +++++- src/tools.py | 3 +- 7 files changed, 143 insertions(+), 21 deletions(-) diff --git a/src/Model/DB.py b/src/Model/DB.py index 013f688e..9405ef78 100644 --- a/src/Model/DB.py +++ b/src/Model/DB.py @@ -38,12 +38,15 @@ class SQLModel(SQL): fn = lambda sql: self.execute( sql, fetch_one = False, - commit = True + commit = False ) for cls in self._sub_classes: requests = cls._sql_create(fn) + self.commit() + return True + def _create(self): raise NotImplementedMethodeError(self, self._create) @@ -51,13 +54,14 @@ class SQLModel(SQL): fn = lambda sql: self.execute( sql, fetch_one = False, - commit = True + commit = False ) ok = True for cls in self._sub_classes: ok &= cls._sql_update(fn, version) + self.commit() return ok def _update(self): @@ -67,13 +71,14 @@ class SQLModel(SQL): fn = lambda sql: self.execute( sql, fetch_one = False, - commit = True + commit = False ) ok = True for obj in objs: ok &= obj._sql_save(fn) + self.commit() return ok def _save(self): diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 9321d5bf..bc878318 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -21,7 +21,8 @@ class PointXYZ(Point, SQLSubModel): def _sql_create(cls, execute): execute(""" CREATE TABLE geometry_pointXYZ( - id INTEGER NOT NULL PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ind INTEGER NOT NULL, name TEXT, x INTEGER NOT NULL, y INTEGER NOT NULL, @@ -41,9 +42,54 @@ class PointXYZ(Point, SQLSubModel): @classmethod def _sql_load(cls, execute, data = None): - return None + points = [] + + status = data["status"] + profile = data["profile"] + + table = execute( + "SELECT ind, name, x, y, z " + + "FROM geometry_pointXYZ " + + f"WHERE profile = {profile}" + ) + + # Create points list + for _ in table: + points.append(None) + + # Fill points list with new point + for row in table: + ind = row[0] + name = row[1] + x = row[2] + y = row[3] + z = row[4] + + new = cls( + name = name, + x = x, y = y, z = z, + status = status + ) + + points[ind] = new + + return points def _sql_save(self, execute, data = None): + profile = data["profile"] + ind = data["ind"] + + sql = ( + "INSERT OR REPLACE INTO " + + "geometry_pointXYZ(ind, name, x, y, z, profile) "+ + "VALUES (" + + f"{ind}, '{self._sql_format(self._name)}', " + + f"{self.x}, {self.y}, {self.z}, " + + f"{profile}" + + ")" + ) + execute(sql) + return True diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index f2678c68..93e2c8a8 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -6,15 +6,23 @@ from Model.Geometry.Point import Point from Model.Except import NotImplementedMethodeError class Profile(object): - def __init__(self, num: int = 0, + _id_cnt = 0 + + def __init__(self, id:int = -1, num:int = 0, kp:float = 0.0, name:str = "", - code1: int = 0, code2: int = 0, + code1:int = 0, code2:int = 0, _type:str = "", reach = None, status = None): super(Profile, self).__init__() self._status = status + if id == -1: + self.id = Profile._id_cnt + Profile._id_cnt += 1 + else: + self.id = id + self._num = int(num) self._code1 = int(code1) self._code2 = int(code2) diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 2013dea3..b583fa8f 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -17,6 +17,7 @@ class ProfileXYZ(Profile, SQLSubModel): ] def __init__(self, + id: int = -1, name: str = "", kp: float = 0., reach = None, @@ -37,6 +38,7 @@ class ProfileXYZ(Profile, SQLSubModel): Nothing. """ super(ProfileXYZ, self).__init__( + id = id, num = num, name = name, kp = kp, @@ -61,20 +63,72 @@ class ProfileXYZ(Profile, SQLSubModel): ) """) - cls._create_submodel(execute) - return True + return cls._create_submodel(execute) @classmethod def _sql_update(cls, execute, version): - cls._update_submodel(execute, version) - return True + return cls._update_submodel(execute, version) @classmethod def _sql_load(cls, execute, data = None): - return None + profiles = [] + status = data["status"] + reach = data["reach"] + + table = execute( + "SELECT id, name, kp, num, code1, code2 " + + "FROM geometry_profileXYZ " + + f"WHERE reach = {reach}" + ) + + for row in table: + id = row[0] + name = row[1] + kp = row[2] + num = row[3] + code1 = row[4] + code2 = row[5] + + new = cls( + id=id, num = num, + name = name, kp = kp, + code1 = code1, code2 = code2, + reach = data["parent"], + status = status + ) + + data["profile"] = id + new._points = PointXYZ._sql_load(execute, data) + + profiles.append(new) + + return profiles def _sql_save(self, execute, data = None): - return True + ok = True + + sql = ( + "INSERT OR REPLACE INTO " + + "geometry_profileXYZ(id, name, reach, kp, num, code1, code2) "+ + "VALUES (" + + f"{self.id}, '{self._sql_format(self._name)}', " + + f"{self.reach.id}, {self.kp}, {self.num}, " + + f"{self.code1}, {self.code1}" + + ")" + ) + execute(sql) + + if data is None: + data = {} + data["profile"] = self.id + + ind = 0 + for point in self._points: + data["ind"] = ind + ok &= point._sql_save(execute, data) + ind += 1 + + return ok @classmethod def from_data(cls, header, data): diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index c5415c76..4e69f582 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -23,6 +23,7 @@ class Reach(SQLSubModel): ] def __init__(self, status=None, parent=None): + self.id = parent.id self._status = status self._parent = parent self._profiles: List[Profile] = [] @@ -46,17 +47,15 @@ class Reach(SQLSubModel): new._profiles = ProfileXYZ._sql_load( execute, - data = { - "status": data["status"], - "reach": new, - } + data = data ) return new def _sql_save(self, execute, data = None): - cls._save_submodel(execute, data) - return True + objs = self._profiles + + return self._save_submodel(execute, objs, data) def profile(self, i): """Returns profile at index i diff --git a/src/Model/River.py b/src/Model/River.py index 452773c3..16095fc4 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -127,6 +127,9 @@ class RiverReach(Edge, SQLSubModel): def _sql_load(cls, execute, data = None): reachs = [] + if data is None: + data = {} + table = execute("SELECT id, name, enable, node1, node2 FROM river_reach") for row in table: # Update id counter @@ -141,6 +144,11 @@ class RiverReach(Edge, SQLSubModel): new = cls(id, name, node1, node2, status = data["status"]) new.enable(enable = enable) + + data["reach"] = id + data["parent"] = new + new._reach = Reach._sql_load(execute, data) + reachs.append(new) return reachs @@ -157,7 +165,8 @@ class RiverReach(Edge, SQLSubModel): ) execute(sql) - return True + objs = [self._reach] + return self._save_submodel(execute, objs, data) @property def reach(self): diff --git a/src/tools.py b/src/tools.py index b54616d1..1c72b031 100644 --- a/src/tools.py +++ b/src/tools.py @@ -231,8 +231,9 @@ class SQL(object): value = value.replace("'", "'") return value + @timer def execute(self, cmd, fetch_one = True, commit = False): - print(f"[SQL] {cmd}") + #print(f"[SQL] {cmd}") res = self._cur.execute(cmd) if commit: From a2ebcadb99501f286bf64f0e08edeb9f7950c222 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 30 Jun 2023 15:03:38 +0200 Subject: [PATCH 006/289] Model, Stricklers: Add SQL export for stricklers. --- src/Model/Geometry/PointXYZ.py | 3 +- src/Model/Geometry/Reach.py | 6 +- src/Model/River.py | 7 ++- src/Model/Stricklers/Stricklers.py | 78 ++++++++++++++++++++++++-- src/Model/Stricklers/StricklersList.py | 33 ++++++++++- src/tools.py | 2 +- 6 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index bc878318..0fecd1ea 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -32,8 +32,7 @@ class PointXYZ(Point, SQLSubModel): ) """) - cls._create_submodel(execute) - return True + return cls._create_submodel(execute) @classmethod def _sql_update(cls, execute, version): diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 4e69f582..c0140db9 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -33,13 +33,11 @@ class Reach(SQLSubModel): @classmethod def _sql_create(cls, execute): - cls._create_submodel(execute) - return True + return cls._create_submodel(execute) @classmethod def _sql_update(cls, execute, version): - cls._update_submodel(execute, version) - return None + return cls._update_submodel(execute, version) @classmethod def _sql_load(cls, execute, data = None): diff --git a/src/Model/River.py b/src/Model/River.py index 16095fc4..2b8f88fa 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -183,7 +183,7 @@ class River(Graph, SQLSubModel): BoundaryConditionList, LateralContributionList, # InitialConditionsDict, - # StricklersList, + StricklersList, # SolverParametersList, ] @@ -236,6 +236,10 @@ class River(Graph, SQLSubModel): execute, data ) + new._stricklers = StricklersList._sql_load( + execute, + data + ) return new @@ -243,6 +247,7 @@ class River(Graph, SQLSubModel): objs = (self._nodes + self._edges) objs.append(self._boundary_condition) objs.append(self._lateral_contribution) + objs.append(self._stricklers) self._save_submodel(execute, objs, data) return True diff --git a/src/Model/Stricklers/Stricklers.py b/src/Model/Stricklers/Stricklers.py index 4eea5036..f205048f 100644 --- a/src/Model/Stricklers/Stricklers.py +++ b/src/Model/Stricklers/Stricklers.py @@ -2,17 +2,83 @@ from tools import trace, timer -class Stricklers(object): - def __init__(self, status = None): +from Model.DB import SQLSubModel + +class Stricklers(SQLSubModel): + def __init__(self, name:str = "", + comment:str = "", + minor:float = 35.0, + medium:float = 15.0, + status = None): super(Stricklers, self).__init__() self._status = status - self._name = "" - self._comment = "" + self._name = name + self._comment = comment - self._minor = 35 - self._medium = 15 + self._minor = minor + self._medium = medium + + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE stricklers( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT, + comment TEXT, + minor REAL NOT NULL, + medium REAL NOT NULL + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + stricklers = [] + status = data["status"] + + table = execute( + "SELECT name, comment, minor, medium " + + "FROM stricklers" + ) + + for row in table: + name = row[0] + comment = row[1] + minor = row[2] + medium = row[3] + + new = cls( + name = name, + comment = comment, + minor = minor, medium = medium, + status = status + ) + + stricklers.append(new) + + return stricklers + + def _sql_save(self, execute, data = None): + sql = ( + "INSERT INTO " + + "stricklers(name, comment, minor, medium) "+ + "VALUES (" + + f"'{self._sql_format(self.name)}', " + + f"'{self._sql_format(self.comment)}', " + + f"{float(self.minor)}, {float(self.medium)}" + + ")" + ) + execute(sql) + + return True def __str__(self): if self._name != "": diff --git a/src/Model/Stricklers/StricklersList.py b/src/Model/Stricklers/StricklersList.py index d2647823..6ed1269d 100644 --- a/src/Model/Stricklers/StricklersList.py +++ b/src/Model/Stricklers/StricklersList.py @@ -2,10 +2,15 @@ from tools import trace, timer +from Model.DB import SQLSubModel from Model.Saved import SavedStatus from Model.Stricklers.Stricklers import Stricklers -class StricklersList(object): +class StricklersList(SQLSubModel): + _sub_classes = [ + Stricklers, + ] + def __init__(self, status = None): if status is None: status = SavedStatus() @@ -13,6 +18,32 @@ class StricklersList(object): self._stricks = [] + @classmethod + def _sql_create(cls, execute): + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls(status = data["status"]) + + new._stricks = Stricklers._sql_load( + execute, + data = data + ) + + return new + + def _sql_save(self, execute, data = None): + if len(self._stricks) != 0: + execute("DELETE FROM stricklers") + + objs = self._stricks + return self._save_submodel(execute, objs, data) + def __len__(self): return len(self._stricks) diff --git a/src/tools.py b/src/tools.py index 1c72b031..27127754 100644 --- a/src/tools.py +++ b/src/tools.py @@ -233,7 +233,7 @@ class SQL(object): @timer def execute(self, cmd, fetch_one = True, commit = False): - #print(f"[SQL] {cmd}") + print(f"[SQL] {cmd}") res = self._cur.execute(cmd) if commit: From 8671e7b7dd0705e1476108a984a4f35f09eb8345 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 30 Jun 2023 16:04:03 +0200 Subject: [PATCH 007/289] Model, SolverParameters: Add SQL export for solver parameters. --- src/Model/River.py | 14 ++- .../SolverParameters/SolverParametersList.py | 88 +++++++++++++++++-- src/Solver/ASolver.py | 2 + src/Solver/GenericSolver.py | 2 + src/Solver/Mage.py | 6 ++ 5 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/Model/River.py b/src/Model/River.py index 2b8f88fa..8a4f6fc6 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -184,7 +184,7 @@ class River(Graph, SQLSubModel): LateralContributionList, # InitialConditionsDict, StricklersList, - # SolverParametersList, + SolverParametersList, ] def __init__(self, status=None): @@ -232,15 +232,25 @@ class River(Graph, SQLSubModel): execute, data ) + # Lateral Contribution new._lateral_contribution = LateralContributionList._sql_load( execute, data ) + # Stricklers new._stricklers = StricklersList._sql_load( execute, data ) + # Parameters + for solver in new._parameters: + data["solver"] = solver + new._parameters[solver] = SolverParametersList( + execute, + data + ) + return new def _sql_save(self, execute, data = None): @@ -248,6 +258,8 @@ class River(Graph, SQLSubModel): objs.append(self._boundary_condition) objs.append(self._lateral_contribution) objs.append(self._stricklers) + for solver in self._parameters: + objs.append(self._parameters[solver]) self._save_submodel(execute, objs, data) return True diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index ee3b9b40..abac7c40 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -3,12 +3,16 @@ from copy import copy from tools import trace, timer -class Parameter(object): - def __init__(self, status = None): +from Model.DB import SQLSubModel + +class Parameter(): + def __init__(self, + name = "", value = "", + status = None): self._status = status - self._name = "" - self._value = "" + self._name = name + self._value = value @property def name(self): @@ -42,11 +46,12 @@ class Parameter(object): return new -class SolverParametersList(object): +class SolverParametersList(SQLSubModel): def __init__(self, solver_type = None, status = None): super(SolverParametersList, self).__init__() self._status = status + self._solver = solver_type self._parameters = list( map( lambda t: Parameter.from_tuple(t, self._status), @@ -54,6 +59,79 @@ class SolverParametersList(object): ) ) + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE solver_parameter( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ind INTEGER NOT NULL, + name TEXT NOT NULL, + value TEXT NOT NULL, + solver TEXT NOT NULL + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + status = data["status"] + solver = data["solver"] + + new = cls(solver_type = solver, status = status) + + table = execute( + "SELECT ind, name, value " + + "FROM solver_parameter " + + f"WHERE solver = '{solver.type}'" + ) + + for _ in table: + new._parameters.append(None) + + for row in table: + ind = row[0] + name = row[1] + value = row[2] + + p = cls( + name = name, + value = value, + status = status + ) + new._parameters[ind] = p + + return stricklers + + def _sql_save(self, execute, data = None): + t = self._solver._type + execute( + "DELETE FROM solver_parameter " + + f"WHERE solver = '{t}'" + ) + + ind = 0 + for param in self._parameters: + sql = ( + "INSERT INTO " + + "solver_parameter(ind, name, value, solver) "+ + "VALUES (" + + f"{ind}, " + + f"'{self._sql_format(param.name)}', " + + f"'{self._sql_format(param.value)}', " + + f"'{self._sql_format(t)}'" + + ")" + ) + execute(sql) + ind += 1 + + return True + def __len__(self): return len(self._parameters) diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index 66689a24..3b8419c7 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -19,6 +19,8 @@ class STATUS(Enum): PAUSED = 5 class AbstractSolver(object): + _type = "" + def __init__(self, name): super(AbstractSolver, self).__init__() diff --git a/src/Solver/GenericSolver.py b/src/Solver/GenericSolver.py index 7cf3e332..a80adf23 100644 --- a/src/Solver/GenericSolver.py +++ b/src/Solver/GenericSolver.py @@ -5,6 +5,8 @@ from Solver.ASolver import ( ) class GenericSolver(AbstractSolver): + _type = "generic" + def __init__(self, name): super(GenericSolver, self).__init__(name) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 6c164d0f..78a89931 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -8,6 +8,8 @@ from Solver.ASolver import AbstractSolver from Checker.Mage import MageNetworkGraphChecker class Mage(AbstractSolver): + _type = "mage" + def __init__(self, name): super(Mage, self).__init__(name) @@ -310,6 +312,8 @@ class Mage(AbstractSolver): ########## class Mage7(Mage): + _type = "mage7" + def __init__(self, name): super(Mage7, self).__init__(name) @@ -326,6 +330,8 @@ class Mage7(Mage): ########## class Mage8(Mage): + _type = "mage8" + def __init__(self, name): super(Mage8, self).__init__(name) From d76b09f2240938ec4180df1a7f67523dfe0ca784 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 3 Jul 2023 11:15:29 +0200 Subject: [PATCH 008/289] Model, IC: Add InitialConditions SQL export. --- .../InitialConditions/InitialConditions.py | 165 ++++++++++++++++-- .../InitialConditionsDict.py | 45 ++++- src/Model/River.py | 11 +- 3 files changed, 201 insertions(+), 20 deletions(-) diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py index 57d582c2..9b226487 100644 --- a/src/Model/InitialConditions/InitialConditions.py +++ b/src/Model/InitialConditions/InitialConditions.py @@ -4,33 +4,121 @@ from copy import copy, deepcopy from tools import trace, timer from functools import reduce -class Data(object): - def __init__(self, reach = None, status = None): +from Model.DB 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): super(Data, self).__init__() self._status = status self._reach = reach - self._name = "" - self._comment = "" + self._name = name + self._comment = comment - self._kp = 0.0 - self._discharge = 0.0 + self._kp = kp + self._discharge = discharge self._speed = 0.0 self._elevation = 0.0 - self._height = 0.0 + self._height = height + + if self._kp != 0.0: + self._update_from_kp() + if self._height != 0.0: + self._update_from_height() + if self._discharge != 0.0: + self._update_from_discharge() + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE initial_conditions( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ind INTEGER NOT NULL, + name TEXT NOT NULL, + comment TEXT NOT NULL, + reach INTEGER, + kp REAL NOT NULL, + discharge REAL NOT NULL, + height REAL NOT NULL, + FOREIGN KEY(reach) REFERENCES river_reach(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + id = data["reach"].id + table = execute( + "SELECT ind, name, comment, kp, discharge, height " + + "FROM initial_conditions " + + f"WHERE reach = {id}" + ) + + new = [] + + for _ in table: + new.append(None) + + for row in table: + ind = row[0] + name = row[1] + comment = row[2] + kp = row[3] + discharge = row[4] + height = row[5] + + d = cls( + 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): + ind = data["ind"] + + execute( + "INSERT INTO " + + "initial_conditions(ind, name, comment, kp, discharge, height, reach) " + + "VALUES (" + + f"{ind}, '{self._sql_format(self.name)}', " + + f"'{self._sql_format(self._comment)}', " + + f"{self._kp}, {self._discharge}, {self._height}, " + + f"{self._reach.id}" + + ")" + ) + + return True + def copy(self): - new = Data(self._reach, self._status) - - new._name = self._name - new._comment = self._comment - new._kp = self._kp - new._discharge = self._discharge - new._speed = self._speed - new._elevation = self._elevation - new._height = self._height + new = Data( + name = self.name, + comment = self._comment, + kp = self._kp, + discharge = self._discharge, + height = self._height, + reach = self._reach, + status = self._status, + ) return new @@ -92,8 +180,8 @@ class Data(object): self._kp = float(value) self._update_from_kp() elif key == "speed": - self._speed = float(value) # Not supposed to be modified + self._speed = float(value) elif key == "discharge": self._discharge = float(value) self._update_from_discharge() @@ -106,7 +194,12 @@ class Data(object): self._status.modified() -class InitialConditions(object): + +class InitialConditions(SQLSubModel): + _sub_classes = [ + Data + ] + def __init__(self, reach = None, status = None): super(InitialConditions, self).__init__() @@ -115,6 +208,42 @@ class InitialConditions(object): self._reach = reach self._data = [] + @classmethod + def _sql_create(cls, execute): + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls( + reach = data["reach"], + status = data["status"] + ) + + new._data = Data._sql_load( + execute, + data = data + ) + + if new._data is None: + return None + + return new + + def _sql_save(self, execute, data = None): + ok = True + + ind = 0 + for d in self._data: + data["ind"] = ind + ok &= d._sql_save(execute, data) + ind += 1 + + return ok + def __len__(self): return len(self._data) diff --git a/src/Model/InitialConditions/InitialConditionsDict.py b/src/Model/InitialConditions/InitialConditionsDict.py index 71d707d0..5e8d5fba 100644 --- a/src/Model/InitialConditions/InitialConditionsDict.py +++ b/src/Model/InitialConditions/InitialConditionsDict.py @@ -3,9 +3,14 @@ from copy import copy from tools import trace, timer +from Model.DB import SQLSubModel from Model.InitialConditions.InitialConditions import InitialConditions -class InitialConditionsDict(object): +class InitialConditionsDict(SQLSubModel): + _sub_classes = [ + InitialConditions, + ] + def __init__(self, status = None): super(InitialConditionsDict, self).__init__() @@ -13,6 +18,44 @@ class InitialConditionsDict(object): self._reach = {} + @classmethod + def _sql_create(cls, execute): + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return cls._update_submodel(execute, version) + + @classmethod + 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 + ) + + if ic is not None: + new._reach[reach] = ic + + return new + + def _sql_save(self, execute, data = None): + ok = True + if data is None: + data = {} + + execute("DELETE FROM initial_conditions") + + for reach in self._reach: + data["reach"] = reach + ok &= self._reach[reach]._sql_save(execute, data) + + return ok + def __len__(self): return len(self._reach) diff --git a/src/Model/River.py b/src/Model/River.py index 8a4f6fc6..15f3e6e4 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -182,7 +182,7 @@ class River(Graph, SQLSubModel): RiverReach, BoundaryConditionList, LateralContributionList, - # InitialConditionsDict, + InitialConditionsDict, StricklersList, SolverParametersList, ] @@ -237,6 +237,13 @@ class River(Graph, SQLSubModel): execute, data ) + + # Initial conditions + new._initial_conditions = InitialConditionsDict._sql_load( + execute, + data + ) + # Stricklers new._stricklers = StricklersList._sql_load( execute, @@ -256,8 +263,10 @@ class River(Graph, SQLSubModel): def _sql_save(self, execute, data = None): objs = (self._nodes + self._edges) objs.append(self._boundary_condition) + objs.append(self._initial_conditions) objs.append(self._lateral_contribution) objs.append(self._stricklers) + for solver in self._parameters: objs.append(self._parameters[solver]) From 8ca1ac8fcf5bed2eace4297babff37963789c9a8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 3 Jul 2023 11:15:53 +0200 Subject: [PATCH 009/289] Model, geometry: Monir change. --- src/Model/Stricklers/Stricklers.py | 3 +++ src/Model/Stricklers/StricklersList.py | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Model/Stricklers/Stricklers.py b/src/Model/Stricklers/Stricklers.py index f205048f..09ecbe67 100644 --- a/src/Model/Stricklers/Stricklers.py +++ b/src/Model/Stricklers/Stricklers.py @@ -49,6 +49,9 @@ class Stricklers(SQLSubModel): "FROM stricklers" ) + if table is None: + return None + for row in table: name = row[0] comment = row[1] diff --git a/src/Model/Stricklers/StricklersList.py b/src/Model/Stricklers/StricklersList.py index 6ed1269d..049d9b83 100644 --- a/src/Model/Stricklers/StricklersList.py +++ b/src/Model/Stricklers/StricklersList.py @@ -38,8 +38,7 @@ class StricklersList(SQLSubModel): return new def _sql_save(self, execute, data = None): - if len(self._stricks) != 0: - execute("DELETE FROM stricklers") + execute("DELETE FROM stricklers") objs = self._stricks return self._save_submodel(execute, objs, data) From 1b9115ef876e1c4f1e83d973d84c3f154c857167 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 3 Jul 2023 11:16:13 +0200 Subject: [PATCH 010/289] Model, SQL: Some minor change for bug fix in SQL export/load. --- src/Model/BoundaryCondition/BoundaryCondition.py | 4 ++-- src/Model/Geometry/ProfileXYZ.py | 2 ++ src/Model/LateralContribution/LateralContribution.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index e88a4f8f..79bcb775 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -16,7 +16,7 @@ class BoundaryCondition(SQLSubModel): self._status = status if id == -1: - self.id = type(self)._id_cnt + self.id = BoundaryCondition._id_cnt else: self.id = id @@ -27,7 +27,7 @@ class BoundaryCondition(SQLSubModel): self._header = [] self._types = [float, float] - type(self)._id_cnt = max(type(self)._id_cnt + 1, self.id) + BoundaryCondition._id_cnt = max(BoundaryCondition._id_cnt + 1, self.id) @classmethod def _sql_create(cls, execute): diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index b583fa8f..9894cfef 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -122,6 +122,8 @@ class ProfileXYZ(Profile, SQLSubModel): data = {} data["profile"] = self.id + execute(f"DELETE FROM geometry_pointXYZ WHERE profile = {self.id}") + ind = 0 for point in self._points: data["ind"] = ind diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index 22d4ca2f..e5427d05 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -15,7 +15,7 @@ class LateralContribution(SQLSubModel): self._status = status if id == -1: - self.id = type(self)._id_cnt + self.id = LateralContribution._id_cnt else: self.id = id @@ -29,7 +29,7 @@ class LateralContribution(SQLSubModel): self._header = [] self._types = [float, float] - type(self)._id_cnt = max(type(self)._id_cnt + 1, self.id) + LateralContribution._id_cnt = max(LateralContribution._id_cnt + 1, self.id) @classmethod def _sql_create(cls, execute): From 9cf3177b57bd675c91343047086194ee62d810ac Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 3 Jul 2023 11:46:49 +0200 Subject: [PATCH 011/289] Model, SQL: Fix solver parameters loading. --- src/Model/River.py | 10 ++-- .../SolverParameters/SolverParametersList.py | 60 ++++++++++++------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/Model/River.py b/src/Model/River.py index 15f3e6e4..da323b6c 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -251,12 +251,10 @@ class River(Graph, SQLSubModel): ) # Parameters - for solver in new._parameters: - data["solver"] = solver - new._parameters[solver] = SolverParametersList( - execute, - data - ) + new._parameters = SolverParametersList._sql_load( + execute, + data + ) return new diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index abac7c40..88d1d567 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -2,6 +2,7 @@ from copy import copy from tools import trace, timer +from Solver.Solvers import solver_type_list from Model.DB import SQLSubModel @@ -81,32 +82,33 @@ class SolverParametersList(SQLSubModel): @classmethod def _sql_load(cls, execute, data = None): status = data["status"] - solver = data["solver"] + solvers = execute("SELECT DISTINCT solver FROM solver_parameter") + new = {} - new = cls(solver_type = solver, status = status) + for solver in solvers: + solver = solver[0] + if solver not in solver_type_list: + continue - table = execute( - "SELECT ind, name, value " + - "FROM solver_parameter " + - f"WHERE solver = '{solver.type}'" - ) + st = solver_type_list[solver] + n = cls(solver_type = st, status = status) - for _ in table: - new._parameters.append(None) - - for row in table: - ind = row[0] - name = row[1] - value = row[2] - - p = cls( - name = name, - value = value, - status = status + table = execute( + "SELECT ind, name, value " + + "FROM solver_parameter " + + f"WHERE solver = '{solver}'" ) - new._parameters[ind] = p - return stricklers + for row in table: + ind = row[0] + name = row[1] + value = row[2] + + n.set_value(name, value) + + new[solver] = n + + return new def _sql_save(self, execute, data = None): t = self._solver._type @@ -146,6 +148,22 @@ class SolverParametersList(SQLSubModel): self._parameters[index] = new self._status.modified() + def set_value(self, key, value): + for p in self._parameters: + if p["name"] == key: + p["value"] = value + self._status.modified() + return + + self._parameters.append( + Parameter( + name = key, + value = value, + status = self._status + ) + ) + self._status.modified() + def new(self, index): n = Parameter(status = self._status) self._parameters.insert(index, n) From 911e0c7e174eec7621657b69008cf13fd83d3253 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 3 Jul 2023 15:41:59 +0200 Subject: [PATCH 012/289] Model, Section: Add SQL export for section. --- src/Model/River.py | 30 ++++++++----- src/Model/Section/Section.py | 71 +++++++++++++++++++++++++++++- src/Model/Section/SectionList.py | 35 ++++++++++++++- src/Model/Stricklers/Stricklers.py | 31 +++++++++---- 4 files changed, 145 insertions(+), 22 deletions(-) diff --git a/src/Model/River.py b/src/Model/River.py index da323b6c..f1aaa613 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -59,7 +59,7 @@ class RiverNode(Node, SQLSubModel): # Update id counter cls._id_cnt = max(cls._id_cnt, row[0]) # Create new node - nodes.append(cls(*row, **data)) + nodes.append(cls(*row, status=data["status"])) return nodes @@ -86,7 +86,7 @@ class RiverNode(Node, SQLSubModel): class RiverReach(Edge, SQLSubModel): _sub_classes = [ Reach, - # SectionList, + SectionList, ] def __init__(self, id:str, name:str, @@ -100,7 +100,7 @@ class RiverReach(Edge, SQLSubModel): ) self._reach = Reach(status=self._status, parent=self) - self._sections = SectionList(status=self._status) + self._sections = SectionList(status = self._status) @classmethod def _sql_create(cls, execute): @@ -149,6 +149,8 @@ class RiverReach(Edge, SQLSubModel): data["parent"] = new new._reach = Reach._sql_load(execute, data) + new._sections = SectionList._sql_load(execute, data) + reachs.append(new) return reachs @@ -165,7 +167,10 @@ class RiverReach(Edge, SQLSubModel): ) execute(sql) - objs = [self._reach] + if data is None: + data = {} + + objs = [self._reach, self._sections] return self._save_submodel(execute, objs, data) @property @@ -213,8 +218,17 @@ class River(Graph, SQLSubModel): @classmethod def _sql_load(cls, execute, data = None): - # Network new = cls(data["status"]) + + # Stricklers (Stricklers is load in first because it's needed + # for reachs) + new._stricklers = StricklersList._sql_load( + execute, + data + ) + data["stricklers"] = new._stricklers + + # Network new._nodes = RiverNode._sql_load( execute, data @@ -244,12 +258,6 @@ class River(Graph, SQLSubModel): data ) - # Stricklers - new._stricklers = StricklersList._sql_load( - execute, - data - ) - # Parameters new._parameters = SolverParametersList._sql_load( execute, diff --git a/src/Model/Section/Section.py b/src/Model/Section/Section.py index 5d541813..82bd454b 100644 --- a/src/Model/Section/Section.py +++ b/src/Model/Section/Section.py @@ -2,8 +2,9 @@ from tools import trace, timer +from Model.DB import SQLSubModel -class Section(object): +class Section(SQLSubModel): def __init__(self, name:str = "", status = None): super(Section, self).__init__() @@ -16,6 +17,74 @@ class Section(object): self._begin_strickler = None self._end_strickler = None + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE section( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ind INTERGER NOT NULL, + begin_kp REAL NOT NULL, + end_kp REAL NOT NULL, + reach INTEGER NOT NULL, + begin_strickler INTERGER, + end_strickler INTERGER, + FOREIGN KEY(reach) REFERENCES river_reach(id), + FOREIGN KEY(begin_strickler) REFERENCES stricklers(id), + FOREIGN KEY(end_strickler) REFERENCES stricklers(id) + ) + """) + + return True + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + reach = data["parent"] # Reach object + status = data["status"] + stricklers = data["stricklers"].stricklers + + table = execute( + "SELECT ind, begin_kp, end_kp, begin_strickler, end_strickler " + + f"FROM section WHERE reach = {reach.id}" + ) + + for _ in table: + new.append(None) + + for row in table: + ind = row[0] + bs = next(filter(lambda s: s.id == row[3], stricklers)) + es = next(filter(lambda s: s.id == row[4], stricklers)) + + sec = cls(status = status) + sec.edge = reach + sec.begin_kp = row[1] + sec.end_kp = row[2] + sec.begin_strickler = bs + sec.end_strickler = es + + new[ind] = sec + + return new + + def _sql_save(self, execute, data = None): + ind = data["ind"] + execute( + "INSERT OR REPLACE INTO " + + "section(ind, begin_kp, end_kp, reach, begin_strickler, end_strickler) " + + "VALUES (" + + f"{ind}, {self._begin_kp}, {self._end_kp}, " + + f"{self._edge.id}, " + + f"{self._begin_strickler.id}, {self._end_strickler.id}" + + ")" + ) + + return True + @property def name(self): return self._name diff --git a/src/Model/Section/SectionList.py b/src/Model/Section/SectionList.py index ff6ad5ef..0f018f66 100644 --- a/src/Model/Section/SectionList.py +++ b/src/Model/Section/SectionList.py @@ -3,15 +3,48 @@ from copy import copy from tools import trace, timer +from Model.DB import SQLSubModel from Model.Section.Section import Section -class SectionList(object): +class SectionList(SQLSubModel): + _sub_classes = [ + Section + ] + def __init__(self, status = None): super(SectionList, self).__init__() self._status = status self._sections = [] + @classmethod + def _sql_create(cls, execute): + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls(status = data['status']) + + new._sections = Section._sql_load( + execute, data + ) + + return new + + def _sql_save(self, execute, data = None): + ok = True + ind = 0 + for section in self._sections: + data["ind"] = ind + ok &= section._sql_save(execute, data = data) + ind += 1 + + return ok + def __len__(self): return len(self._sections) diff --git a/src/Model/Stricklers/Stricklers.py b/src/Model/Stricklers/Stricklers.py index 09ecbe67..cbc10009 100644 --- a/src/Model/Stricklers/Stricklers.py +++ b/src/Model/Stricklers/Stricklers.py @@ -5,15 +5,25 @@ from tools import trace, timer from Model.DB import SQLSubModel class Stricklers(SQLSubModel): - def __init__(self, name:str = "", + _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): super(Stricklers, self).__init__() - self._status = status + if id == -1: + self.id = Stricklers._id_cnt + else: + self.id = id + + Stricklers._id_cnt = max(Stricklers._id_cnt + 1, self.id) + self._name = name self._comment = comment @@ -25,7 +35,7 @@ class Stricklers(SQLSubModel): def _sql_create(cls, execute): execute(""" CREATE TABLE stricklers( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + id INTEGER NOT NULL PRIMARY KEY, name TEXT, comment TEXT, minor REAL NOT NULL, @@ -45,7 +55,7 @@ class Stricklers(SQLSubModel): status = data["status"] table = execute( - "SELECT name, comment, minor, medium " + + "SELECT id, name, comment, minor, medium " + "FROM stricklers" ) @@ -53,12 +63,14 @@ class Stricklers(SQLSubModel): return None for row in table: - name = row[0] - comment = row[1] - minor = row[2] - medium = row[3] + id = row[0] + name = row[1] + comment = row[2] + minor = row[3] + medium = row[4] new = cls( + id = id, name = name, comment = comment, minor = minor, medium = medium, @@ -72,8 +84,9 @@ class Stricklers(SQLSubModel): def _sql_save(self, execute, data = None): sql = ( "INSERT INTO " + - "stricklers(name, comment, minor, medium) "+ + "stricklers(id, name, comment, minor, medium) "+ "VALUES (" + + f"{self.id}, " + f"'{self._sql_format(self.name)}', " + f"'{self._sql_format(self.comment)}', " + f"{float(self.minor)}, {float(self.medium)}" + From 96d03ee41a5e3e21ed8e9aae62ef55fd1dec3807 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 3 Jul 2023 15:52:08 +0200 Subject: [PATCH 013/289] SQL: Minor change. --- src/Model/InitialConditions/InitialConditions.py | 2 +- src/Model/River.py | 2 ++ src/Model/Section/Section.py | 4 +++- src/Model/Section/SectionList.py | 3 +++ src/tools.py | 2 +- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py index 9b226487..bb106662 100644 --- a/src/Model/InitialConditions/InitialConditions.py +++ b/src/Model/InitialConditions/InitialConditions.py @@ -169,7 +169,7 @@ class Data(SQLSubModel): def _update_from_discharge(self): min = self._update_get_min() - print("TODO") + # print("TODO") def __setitem__(self, key, value): if key == "name": diff --git a/src/Model/River.py b/src/Model/River.py index f1aaa613..9f5505ad 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -170,6 +170,8 @@ class RiverReach(Edge, SQLSubModel): if data is None: data = {} + data["reach"] = self + objs = [self._reach, self._sections] return self._save_submodel(execute, objs, data) diff --git a/src/Model/Section/Section.py b/src/Model/Section/Section.py index 82bd454b..86bdff48 100644 --- a/src/Model/Section/Section.py +++ b/src/Model/Section/Section.py @@ -57,9 +57,11 @@ class Section(SQLSubModel): for row in table: ind = row[0] + # Get stricklers bs = next(filter(lambda s: s.id == row[3], stricklers)) es = next(filter(lambda s: s.id == row[4], stricklers)) + # Create section sec = cls(status = status) sec.edge = reach sec.begin_kp = row[1] @@ -74,7 +76,7 @@ class Section(SQLSubModel): def _sql_save(self, execute, data = None): ind = data["ind"] execute( - "INSERT OR REPLACE INTO " + + "INSERT INTO " + "section(ind, begin_kp, end_kp, reach, begin_strickler, end_strickler) " + "VALUES (" + f"{ind}, {self._begin_kp}, {self._end_kp}, " + diff --git a/src/Model/Section/SectionList.py b/src/Model/Section/SectionList.py index 0f018f66..d1f88f08 100644 --- a/src/Model/Section/SectionList.py +++ b/src/Model/Section/SectionList.py @@ -36,6 +36,9 @@ class SectionList(SQLSubModel): return new def _sql_save(self, execute, data = None): + reach = data["reach"] + execute(f"DELETE FROM section WHERE reach = {reach.id}") + ok = True ind = 0 for section in self._sections: diff --git a/src/tools.py b/src/tools.py index 27127754..34c4b9b2 100644 --- a/src/tools.py +++ b/src/tools.py @@ -233,7 +233,7 @@ class SQL(object): @timer def execute(self, cmd, fetch_one = True, commit = False): - print(f"[SQL] {cmd}") + # print(f"[SQL] {cmd}") res = self._cur.execute(cmd) if commit: From 3778818b58fcae3e14e7d0f2d10c92b6e7cf5f65 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 3 Jul 2023 16:24:53 +0200 Subject: [PATCH 014/289] Study: Save study metadata in SQL. --- src/Model/Study.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Model/Study.py b/src/Model/Study.py index 50edc5ce..32893e36 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -83,6 +83,7 @@ class Study(SQLModel): def filename(self, filename): self._filename = str(filename) self._init_db_file(filename, is_new = True) + self.status.modified() @property def time_system(self): @@ -136,6 +137,22 @@ class Study(SQLModel): f"INSERT INTO info VALUES ('version', '{self._sql_format(self._version)}')", 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 ('date', " + + "'{self._date.timestamp()}')" + ) + + self.execute( + f"INSERT INTO info VALUES ('creation_date', " + + "'{self.creation_time.timestamp()}')" + ) + self.execute( + f"INSERT INTO info VALUES ('last_save_date', " + + "'{self.last_save_time.timestamp()}')" + ) self._create_submodel() self.commit() @@ -159,7 +176,19 @@ class Study(SQLModel): new = cls(init_new = False, filename = filename) # TODO: Load metadata - print("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._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]) + ) + new.last_save_date = datetime.fromtimestamp( + float(new.execute("SELECT value FROM info WHERE key='last_save_date'")[0]) + ) # Load river data new._river = River._sql_load( @@ -174,5 +203,12 @@ class Study(SQLModel): 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='{self._date.timestamp()}' WHERE key='date'") + self.execute(f"UPDATE info SET value='{self.creation_date.timestamp()}' WHERE key='creation_date'") + self.execute(f"UPDATE info SET value='{self.last_save_date.timestamp()}' WHERE key='last_save_date'") + self._save_submodel([self._river]) self.commit() From 304f10c88d88736ab34e79b9c2524844aaa6523d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 3 Jul 2023 17:27:51 +0200 Subject: [PATCH 015/289] MainWindow, Config: Minor change and bug fixe for windows config. --- src/View/MainWindow.py | 6 +++--- src/View/Network/GraphWidget.py | 18 ++++++++++-------- src/View/ui/MainWindow.ui | 17 +++++++---------- src/config.py | 11 ++++++++++- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 49387884..fed284a7 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -41,6 +41,7 @@ no_model_action = [ model_action = [ "action_menu_close", "action_menu_edit", "action_menu_save", "action_menu_save_as", "action_toolBar_close", "action_toolBar_save", + "action_menu_numerical_parameter", ] other_model_action = [ @@ -117,6 +118,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_open": self.open_model, "action_menu_save": self.save_study, "action_menu_save_as": self.save_as_study, + "action_menu_numerical_parameter": self.open_solver_parameters, "action_run_solver": self.run_solver, ## Help "action_menu_about": self.open_about, @@ -125,9 +127,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_toolBar_open": self.open_model, "action_toolBar_save": self.save_study, "action_toolBar_close": self.close_model, - "action_toolBar_run_solver": self.open_dummy, - "action_toolBar_kill_solver": self.open_dummy, - "action_toolBar_listing": self.open_dummy, + "action_toolBar_run_solver": self.run_solver, ## Current actions "action_toolBar_network": self.open_network, "action_toolBar_geometry": self.open_geometry, diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index 26aa87c2..2569497a 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -279,6 +279,7 @@ class GraphWidget(QGraphicsView): self._selected_item = None self._selected_new_edge_src_node = None self._current_edge = None + self._current_moved_node = None self.tmp_line = None self.node_items = [] @@ -714,15 +715,16 @@ class GraphWidget(QGraphicsView): self.clicked = False if self._state == "move": - pos = self.mapToScene(event.pos()) - self._undo.push( - SetNodePosCommand( - self._current_moved_node, - (pos.x(), pos.y()), - (self._mouse_origin_x, - self._mouse_origin_y) + if self._current_moved_node is not None: + pos = self.mapToScene(event.pos()) + self._undo.push( + SetNodePosCommand( + self._current_moved_node, + (pos.x(), pos.y()), + (self._mouse_origin_x, + self._mouse_origin_y) + ) ) - ) self.update() super(GraphWidget, self).mouseReleaseEvent(event) diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index fa4be386..777288fe 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -151,10 +151,8 @@ &Execute - + - - @@ -263,8 +261,6 @@ - - @@ -298,8 +294,6 @@ - - @@ -504,9 +498,12 @@ XYZ - + - Numerical parameter for MAGE solver + Numerical parameter for solvers + + + Numerical parameter for solvers @@ -696,7 +693,7 @@ - ressources/gtk-execute.pngressources/gtk-execute.png + ressources/player_play.pngressources/player_play.png Run solver diff --git a/src/config.py b/src/config.py index 64f5664a..16421b8d 100644 --- a/src/config.py +++ b/src/config.py @@ -19,6 +19,8 @@ class Config(SQL): self.filename = Config.filename() self.set_default_value() + print(self.filename) + super(Config, self).__init__(filename = self.filename) def _create(self): @@ -226,7 +228,14 @@ class Config(SQL): @classmethod def filename(cls): - return os.path.expanduser('~') + config_dir + config_file + file = "" + + if os.name == 'posix': + file = os.path.expanduser('~') + config_dir + config_file + else: + file = os.path.expanduser('~') + config_file + + return file @classmethod def languages(cls): From fe7f9ad070fd87c098516262a2d14ef2140834fd Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 09:42:48 +0200 Subject: [PATCH 016/289] MainWindow: Connect some action and disable unimplemented action. --- src/View/MainWindow.py | 14 ++++++-- src/View/ui/MainWindow.ui | 67 ++++++++++++++++++++++++++++++++------- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index fed284a7..1b5f06e3 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -57,7 +57,11 @@ define_model_action = [ "action_toolBar_stricklers", "action_toolBar_building", "action_toolBar_initial_cond", # Menu - "action_run_solver", + "action_menu_run_solver", "action_menu_numerical_parameter", + "action_menu_edit_network", "action_menu_edit_geometry", + "action_menu_boundary_conditions", "action_menu_initial_conditions", + "action_menu_edit_friction", "action_menu_edit_lateral_contribution", + "action_menu_run_solver", ] action = ( @@ -119,7 +123,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_save": self.save_study, "action_menu_save_as": self.save_as_study, "action_menu_numerical_parameter": self.open_solver_parameters, - "action_run_solver": self.run_solver, + "action_menu_edit_network": self.open_network, + "action_menu_edit_geometry": self.open_geometry, + "action_menu_boundary_conditions": self.open_boundary_cond, + "action_menu_initial_conditions": self.open_initial_conditions, + "action_menu_edit_friction": self.open_sections, + "action_menu_edit_lateral_contribution": self.open_lateral_contrib, + "action_menu_run_solver": self.run_solver, ## Help "action_menu_about": self.open_about, # ToolBar action diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 777288fe..619b87e6 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -152,7 +152,7 @@ &Execute - + @@ -163,14 +163,14 @@ &Hydraulics - + - + - - + + @@ -345,6 +345,9 @@ + + false + Import data from MAGE @@ -353,6 +356,9 @@ + + false + Import data from RubarBE @@ -437,16 +443,25 @@ + + false + Import geometry + + false + Export geometry + + false + Run extrenal meshing tool @@ -489,11 +504,17 @@ + + false + Abscisse - Cote + + false + XYZ @@ -506,7 +527,7 @@ Numerical parameter for solvers - + Boundary conditions and one-time contributions @@ -517,7 +538,7 @@ - + Initial conditions @@ -526,26 +547,35 @@ true + + false + Export initial conditions + + false + Import final state as initial condition - + Edit friction - + Edit lateral contributions + + false + Edit spills @@ -556,11 +586,14 @@ + + false + Edit cross building - + Run solver @@ -576,6 +609,9 @@ + + false + Simulation directory management @@ -591,6 +627,9 @@ + + false + Hydrograph @@ -603,6 +642,9 @@ + + false + Limnigram @@ -614,6 +656,9 @@ + + false + Map current reach @@ -625,7 +670,7 @@ - help MAGE + Help MAGE From b95e015f01925884a8ed16e6f2d947e3e360a5c5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 09:52:46 +0200 Subject: [PATCH 017/289] CheckList: Can automatically run solver if no error are detected. --- src/View/CheckList/Window.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index 14259cf9..55edfe8b 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -33,10 +33,13 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): signalStatus = pyqtSignal(str) def __init__(self, title="Check list", + 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 @@ -142,6 +145,8 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): errors = any(filter(lambda c: c.is_error(), self._checker_list)) if not errors: self.find(QPushButton, "pushButton_ok").setEnabled(True) + if self._autorun: + self._parent.solver_log(self._solver) self.update_statusbar() From f394989a5cfdd790be9235d7d72bf5051ee83a85 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 09:53:55 +0200 Subject: [PATCH 018/289] SolverRun: Add solver type name at solver choose combobox. --- src/View/RunSolver/Window.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index b1c5a74e..822f2bb7 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -57,7 +57,7 @@ class SelectSolverWindow(ASubWindow, ListedSubWindow): def setup_combobox(self): solvers = self._config.solvers - solvers_name = list(map(lambda s: s.name, solvers)) + solvers_name = list(map(lambda s: s.name + f" - ({s._type})", solvers)) self.combobox_add_items("comboBox", solvers_name) @@ -71,6 +71,8 @@ class SelectSolverWindow(ASubWindow, ListedSubWindow): def accept(self): solver_name = self.get_combobox_text("comboBox") + solver_name = solver_name.rsplit(" - ", 1)[0] + self._solver = next( filter( lambda s: s.name == solver_name, From e7099bfb862b493f7f5d2bd7b65c0652af5e7753 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 13:26:45 +0200 Subject: [PATCH 019/289] Profile: Fix profile id issues and profile index in reach. --- src/Model/Geometry/Profile.py | 3 ++- src/Model/Geometry/ProfileXYZ.py | 27 +++++++++++++++------------ src/Model/Geometry/Reach.py | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 93e2c8a8..4a1f5a5f 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -19,10 +19,11 @@ class Profile(object): if id == -1: self.id = Profile._id_cnt - Profile._id_cnt += 1 else: self.id = id + Profile._id_cnt = max(self.id, Profile._id_cnt+1) + self._num = int(num) self._code1 = int(code1) self._code2 = int(code2) diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 9894cfef..d368b981 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -53,6 +53,7 @@ class ProfileXYZ(Profile, SQLSubModel): execute(""" CREATE TABLE geometry_profileXYZ( id INTEGER NOT NULL PRIMARY KEY, + ind INTEGER NOT NULL, name TEXT, reach INTEGER NOT NULL, kp REAL NOT NULL, @@ -76,18 +77,22 @@ class ProfileXYZ(Profile, SQLSubModel): reach = data["reach"] table = execute( - "SELECT id, name, kp, num, code1, code2 " + + "SELECT id, ind, name, kp, num, code1, code2 " + "FROM geometry_profileXYZ " + f"WHERE reach = {reach}" ) + for _ in table: + profiles.append(None) + for row in table: id = row[0] - name = row[1] - kp = row[2] - num = row[3] - code1 = row[4] - code2 = row[5] + ind = row[1] + name = row[2] + kp = row[3] + num = row[5] + code1 = row[5] + code2 = row[6] new = cls( id=id, num = num, @@ -100,28 +105,26 @@ class ProfileXYZ(Profile, SQLSubModel): data["profile"] = id new._points = PointXYZ._sql_load(execute, data) - profiles.append(new) + profiles[ind] = new return profiles def _sql_save(self, execute, data = None): ok = True + ind = data["ind"] sql = ( "INSERT OR REPLACE INTO " + - "geometry_profileXYZ(id, name, reach, kp, num, code1, code2) "+ + "geometry_profileXYZ(id, ind, name, reach, kp, num, code1, code2) "+ "VALUES (" + - f"{self.id}, '{self._sql_format(self._name)}', " + + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + f"{self.reach.id}, {self.kp}, {self.num}, " + f"{self.code1}, {self.code1}" + ")" ) execute(sql) - if data is None: - data = {} data["profile"] = self.id - execute(f"DELETE FROM geometry_pointXYZ WHERE profile = {self.id}") ind = 0 diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index c0140db9..fa72c8ec 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -51,9 +51,20 @@ class Reach(SQLSubModel): return new def _sql_save(self, execute, data = None): - objs = self._profiles + # Delete old data + execute(f"DELETE FROM geometry_profileXYZ") + execute(f"DELETE FROM geometry_pointXYZ") - return self._save_submodel(execute, objs, data) + if data is None: + data = {} + + ind = 0 + for profile in self._profiles: + data["ind"] = ind + profile._sql_save(execute, data) + ind += 1 + + return True def profile(self, i): """Returns profile at index i From adc7736306afbeed72c32718e568b70abeffb12f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 13:32:42 +0200 Subject: [PATCH 020/289] MainWindow, Config: Minor change. --- src/View/MainWindow.py | 2 +- src/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 1b5f06e3..f3ab0acb 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -229,7 +229,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "", "Pamhyr(*.pamhyr)" ) - if file_name[-4:] == ".pamhyr": + if file_name.rsplit(".", 1)[-1] == "pamhyr": self.model.filename = file_name else: self.model.filename = file_name + ".pamhyr" diff --git a/src/config.py b/src/config.py index 16421b8d..b58325be 100644 --- a/src/config.py +++ b/src/config.py @@ -218,7 +218,7 @@ class Config(SQL): self.backup_max = 10 # Editor - self.editor = "emacs @file" + self.editor = "editor @file" # Languages self.lang = "" From 39f6baf1a982194022412823327f7d5a2e831bb5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 13:47:48 +0200 Subject: [PATCH 021/289] MainWindow, SolverRun: Add shortcut for solver run. --- src/View/CheckList/Window.py | 2 +- src/View/MainWindow.py | 18 +++++++++++++++--- src/View/ui/CheckList.ui | 6 +++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index 55edfe8b..8f720e0f 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -174,4 +174,4 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): def accept(self): self._parent.solver_log(self._solver) - self.end() + #self.end() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index f3ab0acb..a907426d 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -4,12 +4,16 @@ import os from queue import Queue from PyQt5 import QtGui +from PyQt5.QtGui import ( + QKeySequence, +) + from PyQt5.QtCore import ( QTranslator, QEvent ) from PyQt5.QtWidgets import ( QMainWindow, QApplication, QAction, - QFileDialog, + QFileDialog, QShortcut, ) from PyQt5.uic import loadUi @@ -84,7 +88,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self ) - self.init_callback() + self.setup_sc() + self.setup_connection() self.default_style() self.trans = QTranslator(self) @@ -108,7 +113,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): """ self.findChild(QAction, action).setEnabled(enable) - def init_callback(self): + def setup_sc(self): + self._run_sc = QShortcut(QKeySequence("F5"), self) + + def setup_connection(self): """Connect action to callback function Returns: @@ -157,6 +165,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): .triggered.connect(actions[action]) # action.triggered.connect(actions[action]) + self._run_sc.activated.connect(self.run_solver) def changeEvent(self, event): if event.type() == QEvent.LanguageChange: @@ -386,6 +395,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): params.show() def run_solver(self): + if self.model is None: + return + run = SelectSolverWindow( study = self.model, config = self.conf, diff --git a/src/View/ui/CheckList.ui b/src/View/ui/CheckList.ui index 06e6403c..001d9230 100644 --- a/src/View/ui/CheckList.ui +++ b/src/View/ui/CheckList.ui @@ -63,7 +63,11 @@ false - Ok + Run + + + + ressources/player_play.pngressources/player_play.png From 29986079f376eb87a97032b95c7a8d3606a3c6e8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 14:18:56 +0200 Subject: [PATCH 022/289] Configure: Add debug mode. --- src/View/Configure/Window.py | 8 +++++++ src/View/MainWindow.py | 5 ++-- src/View/ui/ConfigureDialog.ui | 42 +++++++++++++++++++++++----------- src/View/ui/MainWindow.ui | 6 ++--- src/config.py | 10 ++++++-- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index 3e51f5c1..8d941180 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -140,6 +140,7 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): def setup_sc(self): self._undo_stack = QUndoStack() + 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) @@ -173,6 +174,8 @@ 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() @@ -209,6 +212,11 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): self.conf.save() self.close() + # Debug + def set_debug(self): + self.conf.debug = not self.conf.debug + print(f"[DEBUG] Debug mode set : {self.conf.debug}") + # Solvers def edit_solver(self): diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index a907426d..5150409c 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -114,7 +114,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.findChild(QAction, action).setEnabled(enable) def setup_sc(self): - self._run_sc = QShortcut(QKeySequence("F5"), self) + # self._run_sc = QShortcut(QKeySequence("F5"), self) + return def setup_connection(self): """Connect action to callback function @@ -165,7 +166,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): .triggered.connect(actions[action]) # action.triggered.connect(actions[action]) - self._run_sc.activated.connect(self.run_solver) + # self._run_sc.activated.connect(self.run_solver) def changeEvent(self, event): if event.type() == QEvent.LanguageChange: diff --git a/src/View/ui/ConfigureDialog.ui b/src/View/ui/ConfigureDialog.ui index 8f9a5603..bbde334e 100644 --- a/src/View/ui/ConfigureDialog.ui +++ b/src/View/ui/ConfigureDialog.ui @@ -21,6 +21,9 @@ + + true + 0 @@ -37,6 +40,9 @@ false + + true + Solvers @@ -224,6 +230,9 @@ + + false + Backup @@ -414,6 +423,19 @@ Editor + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -422,11 +444,11 @@ - This value must be used for reading or editing files in speficic case + This value must be used for reading or editing files in speficic case. - + @@ -444,18 +466,12 @@ - - - - Qt::Vertical + + + + - The "@file" keyworkd is replace by the path of file to open. - - - 20 - 40 - - - + diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 619b87e6..eb64701c 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -597,6 +597,9 @@ Run solver + + F5 + @@ -746,9 +749,6 @@ Run solver on current study - - Ctrl+X - diff --git a/src/config.py b/src/config.py index b58325be..66d91396 100644 --- a/src/config.py +++ b/src/config.py @@ -19,8 +19,6 @@ class Config(SQL): self.filename = Config.filename() self.set_default_value() - print(self.filename) - super(Config, self).__init__(filename = self.filename) def _create(self): @@ -135,6 +133,10 @@ class Config(SQL): v = self.execute("SELECT value FROM data WHERE key='lang'") self.lang = v[0] + # Debug + v = self.execute("SELECT value FROM data WHERE key='debug'") + self.debug = v[0] == "True" + self._load_solver() self._load_stricklers() @@ -183,6 +185,7 @@ class Config(SQL): "backup_max": self.backup_max, "editor": self.editor, "lang": self.lang, + "debug": self.debug, } for key in data: @@ -226,6 +229,9 @@ class Config(SQL): # Stricklers self.stricklers = StricklersList() + # Debug + self.debug = False + @classmethod def filename(cls): file = "" From 0aeb2f2530f5b3eeed8b2657491adad29f595d8e Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 15:34:35 +0200 Subject: [PATCH 023/289] Debug: Add debug python repl. --- src/View/Configure/Window.py | 1 + src/View/Debug/Window.py | 81 ++++++++++++++++++++++++++++++++++++ src/View/MainWindow.py | 32 +++++++++++++- src/View/ui/DebugRepl.ui | 68 ++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/View/Debug/Window.py create mode 100644 src/View/ui/DebugRepl.ui diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index 8d941180..69d64a69 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -216,6 +216,7 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): def set_debug(self): self.conf.debug = not self.conf.debug print(f"[DEBUG] Debug mode set : {self.conf.debug}") + self.parent.setup_debug_mode() # Solvers diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py new file mode 100644 index 00000000..dd26d91e --- /dev/null +++ b/src/View/Debug/Window.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from View.ASubWindow import ASubMainWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, QRect, QThread, + pyqtSlot, pyqtSignal, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, + QProgressBar, QLabel, QTextEdit, +) + +_translate = QCoreApplication.translate + +class ReplWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="Debug REPL", + 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 + ) + self.ui.setWindowTitle(self._title) + + self.__debug_exec_result__ = None + self._history = [] + self._history_ind = 0 + + self.setup_connections() + + def setup_connections(self): + self._hup_sc = QShortcut(QKeySequence("Up"), self) + self._hdown_sc = QShortcut(QKeySequence("Down"), self) + self._hup_sc.activated.connect(self.history_up) + self._hdown_sc.activated.connect(self.history_down) + + self.find(QPushButton, "pushButton").clicked.connect(self.eval_python) + + def history_up(self): + if self._history_ind < len(self._history): + self._history_ind += 1 + self.set_line_edit_text("lineEdit", self._history[-self._history_ind]) + + def history_down(self): + if self._history_ind > 0: + self._history_ind -= 1 + self.set_line_edit_text("lineEdit", self._history[-self._history_ind]) + + def eval_python(self): + code = self.get_line_edit_text("lineEdit") + self._history.append(code) + self.set_line_edit_text("lineEdit", "") + self._history_ind = 0 + + code = "self.__debug_exec_result__ = " + code + print(f"[DEBUG] ! {code}") + value = exec(code) + value = self.__debug_exec_result__ + + msg = f" # " + code + " #" + self.find(QTextEdit, "textEdit").append(msg) + + self.find(QTextEdit, "textEdit").append(str(value)) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 5150409c..7953f3e3 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -13,7 +13,7 @@ from PyQt5.QtCore import ( ) from PyQt5.QtWidgets import ( QMainWindow, QApplication, QAction, - QFileDialog, QShortcut, + QFileDialog, QShortcut, QMenu, QToolBar, ) from PyQt5.uic import loadUi @@ -34,6 +34,7 @@ from View.Sections.Window import SectionsWindow from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow +from View.Debug.Window import ReplWindow from Model.Study import Study @@ -91,6 +92,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.setup_sc() self.setup_connection() self.default_style() + self.setup_debug_mode(init = True) self.trans = QTranslator(self) #self.ui.retranslateUi() @@ -183,6 +185,22 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # Maximise window self.showMaximized() + def setup_debug_mode(self, init = False): + menu = self.findChild(QMenu, "menu_help") + + if init: + self.debug_action = QAction("Debug", self) + self.debug_action.setStatusTip("Debug") + self.debug_action.triggered.connect(self.open_debug) + + if self.conf.debug: + menu.addAction(self.debug_action) + else: + if self.conf.debug: + menu.addAction(self.debug_action) + else: + menu.removeAction(self.debug_action) + ######### # MODEL # ######### @@ -423,6 +441,18 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) sol.show() + ######### + # DEBUG # + ######### + + def open_debug(self): + repl = ReplWindow( + study = self.model, + config = self.conf, + parent = self + ) + repl.show() + # TODO: Delete me ! ############### # DUMMY STUFF # diff --git a/src/View/ui/DebugRepl.ui b/src/View/ui/DebugRepl.ui new file mode 100644 index 00000000..f889061d --- /dev/null +++ b/src/View/ui/DebugRepl.ui @@ -0,0 +1,68 @@ + + + MainWindow + + + + 0 + 0 + 640 + 480 + + + + MainWindow + + + + + + + + + + + + + + + Eval + + + + ressources/player_play.pngressources/player_play.png + + + Ctrl+Return + + + false + + + + + + + + + true + + + + + + + + + 0 + 0 + 640 + 22 + + + + + + + + From 06de6a3a5b40a42cfc894f87b86a46ee09aa6bba Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 15:44:27 +0200 Subject: [PATCH 024/289] Debug: Add exeception. --- src/View/Debug/Window.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py index dd26d91e..5fad77df 100644 --- a/src/View/Debug/Window.py +++ b/src/View/Debug/Window.py @@ -70,12 +70,14 @@ class ReplWindow(ASubMainWindow, ListedSubWindow): self.set_line_edit_text("lineEdit", "") self._history_ind = 0 - code = "self.__debug_exec_result__ = " + code + rich_code = "self.__debug_exec_result__ = " + code print(f"[DEBUG] ! {code}") - value = exec(code) - value = self.__debug_exec_result__ + try: + value = exec(rich_code) + value = self.__debug_exec_result__ + except Exception as e: + value = f"" + str(e) + "" msg = f" # " + code + " #" self.find(QTextEdit, "textEdit").append(msg) - self.find(QTextEdit, "textEdit").append(str(value)) From 8cdb3010b9c32fc47f9b4f5098be50095598dfc6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 16:15:03 +0200 Subject: [PATCH 025/289] tests_cases: Add Saar study PAMHYR file. --- tests_cases/Saar/Saar.pamhyr | Bin 0 -> 176128 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests_cases/Saar/Saar.pamhyr diff --git a/tests_cases/Saar/Saar.pamhyr b/tests_cases/Saar/Saar.pamhyr new file mode 100644 index 0000000000000000000000000000000000000000..df56a2984353c1c2f9d8f73ffc277b4fab764ba6 GIT binary patch literal 176128 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 b1?E~{t_9{=V6Fw`T41gP=33zY*B1D1^QhA{ literal 0 HcmV?d00001 From e30a5f4d91c8ebb57d355598d7f7059463ab5497 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 16:49:23 +0200 Subject: [PATCH 026/289] Windows: Fix windows version (again...) --- packages/wine.sh | 2 +- src/config.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/wine.sh b/packages/wine.sh index 0194a92a..7febf68e 100755 --- a/packages/wine.sh +++ b/packages/wine.sh @@ -30,7 +30,7 @@ then win10 echo " *** PYTHON INSTALL" - if [ ! -f $WINEPREFIX/drive_c/Program Files/Python38/python.exe ]; + if [ ! -f "$WINEPREFIX/drive_c/Program Files/Python38/python.exe" ]; then #DISPLAY=:42.0 wine cmd /c python-3.8.10-amd64.exe /quiet PrependPath=1 wine cmd /c python-3.8.10-amd64.exe PrependPath=1 diff --git a/src/config.py b/src/config.py index 66d91396..24ebebe9 100644 --- a/src/config.py +++ b/src/config.py @@ -19,6 +19,8 @@ class Config(SQL): self.filename = Config.filename() self.set_default_value() + print(self.filename) + super(Config, self).__init__(filename = self.filename) def _create(self): @@ -239,7 +241,9 @@ class Config(SQL): if os.name == 'posix': file = os.path.expanduser('~') + config_dir + config_file else: - file = os.path.expanduser('~') + config_file + ndir = os.path.expanduser('~') + config_dir.replace("/", "\\") + os.makedirs(ndir, exist_ok = True) + file = ndir + config_file return file From 88f1c15ab46715e0171d28a86913a356265ee483 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 4 Jul 2023 17:25:20 +0200 Subject: [PATCH 027/289] git: Update gitignore. --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 59542f0a..d56e8602 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,18 @@ *.tar.xz *.zip *.spec +*.pkl +*.class + +tmp + +*.tar.* +*.zip pamhyrBuild +wine-pamhyr-build* + +_PAMHYR_ ### END CUSTOM ### From 94509a1638e00eaee0515779499ab46a0172cdcd Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 09:33:11 +0200 Subject: [PATCH 028/289] BC, LC: Fix SQL export. --- src/Model/BoundaryCondition/BoundaryCondition.py | 5 ++++- src/Model/BoundaryCondition/BoundaryConditionList.py | 2 ++ src/Model/LateralContribution/LateralContribution.py | 4 +++- src/Model/LateralContribution/LateralContributionList.py | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index 79bcb775..152a6269 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -96,7 +96,10 @@ class BoundaryCondition(SQLSubModel): name = row[1], status = data['status'] ) - bc.node = next(filter(lambda n: n.id == row[3], data["nodes"])) + + bc.node = None + if row[3] != -1: + bc.node = next(filter(lambda n: n.id == row[3], data["nodes"])) values = execute( "SELECT ind, data0, data1 FROM boundary_condition_data " + diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py index c59f97b0..d14a870b 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionList.py +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -53,6 +53,8 @@ class BoundaryConditionList(SQLSubModel): return new def _sql_save(self, execute, data = None): + execute("DELETE FROM boundary_condition") + if data is None: data = {} diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index e5427d05..5098e9ba 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -96,7 +96,9 @@ class LateralContribution(SQLSubModel): name = row[1], status = data['status'] ) - lc.edge = next(filter(lambda e: e.id == row[3], data["edges"])) + lc.edge = None + if row[3] != -1: + lc.edge = next(filter(lambda e: e.id == row[3], data["edges"])) lc._begin_kp = row[4] lc._end_kp = row[5] diff --git a/src/Model/LateralContribution/LateralContributionList.py b/src/Model/LateralContribution/LateralContributionList.py index 810a3188..4d6a8567 100644 --- a/src/Model/LateralContribution/LateralContributionList.py +++ b/src/Model/LateralContribution/LateralContributionList.py @@ -51,6 +51,8 @@ class LateralContributionList(SQLSubModel): return new def _sql_save(self, execute, data = None): + execute("DELETE FROM lateral_contribution") + if data is None: data = {} From d09665163fe16ce05477fbe74784aa5928c9a238 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 10:30:51 +0200 Subject: [PATCH 029/289] Study: Copy previous version of DB before all save. --- src/Model/Study.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Model/Study.py b/src/Model/Study.py index 32893e36..a732cb86 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import shutil from datetime import datetime from Model.DB import SQLModel @@ -41,6 +42,8 @@ class Study(SQLModel): else: self._init_db_file(filename, is_new = False) + self._old_save_id = 0 + @classmethod def checkers(cls): lst = [ @@ -62,9 +65,27 @@ class Study(SQLModel): return self.status.is_saved() def save(self): + # Save a copy of database + fdir, fname = os.path.split(self.filename) + + if self._old_save_id == 0: + try: + os.makedirs(os.path.join(fdir, "__old__")) + except FileExistsError as e: + shutil.rmtree(os.path.join(fdir, "__old__")) + os.makedirs(os.path.join(fdir, "__old__")) + except Exception as e: + print(e) + + fname = fname + "." + str(self._old_save_id) + filename = os.path.join(fdir, "__old__", fname) + shutil.copy(self.filename, filename) + self._old_save_id += 1 + + # Save self.last_save_date = datetime.now() - self.status.save() self._save() + self.status.save() @property def name(self): From fb06055b11beaac1eca238681a176baa59c1ddde Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 11:25:33 +0200 Subject: [PATCH 030/289] Window: Fix save for windows version. --- src/Model/Study.py | 18 ++++++++++++------ src/tools.py | 9 +++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Model/Study.py b/src/Model/Study.py index a732cb86..ff806264 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -4,6 +4,8 @@ import os import shutil from datetime import datetime +from tools import timer, timestamp + from Model.DB import SQLModel from Model.Saved import SavedStatus from Model.Serializable import Serializable @@ -69,11 +71,15 @@ class Study(SQLModel): fdir, fname = os.path.split(self.filename) if self._old_save_id == 0: + old_dir = os.path.join(fdir, "__old__") + if os.name == "nt": + old_dir = old_dir.replace("/", "\\") + try: - os.makedirs(os.path.join(fdir, "__old__")) + os.makedirs(old_dir) except FileExistsError as e: - shutil.rmtree(os.path.join(fdir, "__old__")) - os.makedirs(os.path.join(fdir, "__old__")) + shutil.rmtree(old_dir) + os.makedirs(old_dir) except Exception as e: print(e) @@ -227,9 +233,9 @@ class Study(SQLModel): 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='{self._date.timestamp()}' WHERE key='date'") - self.execute(f"UPDATE info SET value='{self.creation_date.timestamp()}' WHERE key='creation_date'") - self.execute(f"UPDATE info SET value='{self.last_save_date.timestamp()}' WHERE key='last_save_date'") + 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/tools.py b/src/tools.py index 34c4b9b2..410cb799 100644 --- a/src/tools.py +++ b/src/tools.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- +import os import time import sqlite3 import traceback +from datetime import datetime from pathlib import Path from colorama import Fore @@ -135,6 +137,13 @@ def flatten(lst): return reduce(list.__add__, lst) +def timestamp(dt:datetime): + # Fix timestamp for some windows version. + # - Issue : (https://bugs.python.org/issue29097) + if os.name == 'nt': + return (dt - datetime(1970, 1, 1)).total_seconds() + return dt.timestamp() + def old_pamhyr_date_to_timestamp(date:str): v = date.split(":") if len(v) != 4: From b1ea642f1c641f89521c80f8fe2bbfea06a60051 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 11:30:28 +0200 Subject: [PATCH 031/289] git: Minor change. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d56e8602..fcf5e6a1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ pamhyrBuild wine-pamhyr-build* _PAMHYR_ +__old__ ### END CUSTOM ### From 4ed1aef4a5e676597febc373b4da48b90d5550f8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 11:46:04 +0200 Subject: [PATCH 032/289] MainWindow: Display message when there are no current reach. --- src/View/MainWindow.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 7953f3e3..2691855c 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -289,6 +289,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # SUBWINDOW # ############# + def msg_select_reach(self): + self.message_box("Please select a reach", + "Geometry edition need a reach selected " + "into river network window to work on it") + def open_configure(self): """Open configure window @@ -371,9 +376,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): geometry = GeometryWindow(model=self.model, parent=self) geometry.show() else: - self.message_box("Please select a reach", - "Geometry edition need a reach selected " - "into river network window to work on it") + self.msg_select_reach() def open_boundary_cond(self): bound = BoundaryConditionWindow(study = self.model, parent = self) @@ -392,11 +395,15 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): strick.show() def open_sections(self): - sections = SectionsWindow( - study = self.model, - parent = self - ) - sections.show() + if (self.model is not None and + self.model.river.has_current_reach()): + sections = SectionsWindow( + study = self.model, + parent = self + ) + sections.show() + else: + self.msg_select_reach() def open_initial_conditions(self): if self.model.river.has_current_reach(): @@ -405,6 +412,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): parent = self ) initial.show() + else: + self.msg_select_reach() def open_solver_parameters(self): params = SolverParametersWindow( From e617738fcd55b3343918345e2e4653d03d0840ef Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 14:33:20 +0200 Subject: [PATCH 033/289] MainWindow: Add save question at PAMHYR closed. --- src/View/MainWindow.py | 54 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 2691855c..e763e956 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -14,6 +14,7 @@ from PyQt5.QtCore import ( from PyQt5.QtWidgets import ( QMainWindow, QApplication, QAction, QFileDialog, QShortcut, QMenu, QToolBar, + QMessageBox, ) from PyQt5.uic import loadUi @@ -77,6 +78,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def __init__(self, conf=None): super(ApplicationWindow, self).__init__() + self._close_question = False + # App Configuration self.conf = conf @@ -175,6 +178,24 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.retranslateUi() super(ApplicationWindow, self).changeEvent(event) + def close(self): + if self.model is not None and not self.model.is_saved: + self._close_question = True + if self.dialog_close(): + super(ApplicationWindow, self).close() + else: + self._close_question = False + else: + super(ApplicationWindow, self).close() + + def closeEvent(self, event): + if not self._close_question: + if self.model is not None and not self.model.is_saved: + if self.dialog_close(cancel = False): + super(ApplicationWindow, self).closeEvent(event) + else: + super(ApplicationWindow, self).closeEvent(event) + def default_style(self): """Set default window style @@ -262,6 +283,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): else: self.model.filename = file_name + ".pamhyr" + print("[INFO] Save...") self.model.save() def save_as_study(self): @@ -285,15 +307,41 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.model.save() - ############# - # SUBWINDOW # - ############# + ################## + # MSG AND DIALOG # + ################## def msg_select_reach(self): self.message_box("Please select a reach", "Geometry edition need a reach selected " "into river network window to work on it") + def dialog_close(self, cancel = True): + dlg = QMessageBox(self) + + dlg.setWindowTitle("Close PAMHYR without saving study") + dlg.setText("Do you want to save current study before PAMHYR close ?") + opt = QMessageBox.Save | QMessageBox.Ignore + if cancel: + opt |= QMessageBox.Cancel + + dlg.setStandardButtons(opt) + dlg.setIcon(QMessageBox.Warning) + + res = dlg.exec() + + if res == QMessageBox.Save: + self.save_study() + return True + elif res == QMessageBox.Ignore: + return True + elif res == QMessageBox.Cancel: + return False + + ############# + # SUBWINDOW # + ############# + def open_configure(self): """Open configure window From 9b9ef8ee4bd7dabb4e5bda25bbca1d0a1cb7ea01 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 15:06:28 +0200 Subject: [PATCH 034/289] NewStudy: Add additional infomation into creation/edit windows. --- src/View/Study/Window.py | 12 ++- src/View/ui/NewStudy.ui | 186 +++++++++++++++++++++++---------------- 2 files changed, 122 insertions(+), 76 deletions(-) diff --git a/src/View/Study/Window.py b/src/View/Study/Window.py index 6d313199..a90ed549 100644 --- a/src/View/Study/Window.py +++ b/src/View/Study/Window.py @@ -3,10 +3,13 @@ from Model.Study import Study from View.ASubWindow import ASubWindow +from PyQt5.QtCore import QCoreApplication + from PyQt5.QtWidgets import ( QRadioButton, QLabel, QDateTimeEdit, ) +_translate = QCoreApplication.translate class NewStudyWindow(ASubWindow): def __init__(self, study=None, title="New Study", parent=None): @@ -14,13 +17,20 @@ class NewStudyWindow(ASubWindow): self.ui.setWindowTitle(title) self.parent = parent - self.study = 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) + + self.find(QLabel, "label_creation_date_data").setText( + self.study.creation_date.isoformat(sep=" ") + ) + self.find(QLabel, "label_last_modification_data").setText( + self.study.last_save_date.isoformat(sep=" ") + ) + if self.study.time_system == "date": self.set_radio_button("radioButton_date", True) self.find(QLabel, "label_date").setEnabled(True) diff --git a/src/View/ui/NewStudy.ui b/src/View/ui/NewStudy.ui index 95100b5c..a7892389 100644 --- a/src/View/ui/NewStudy.ui +++ b/src/View/ui/NewStudy.ui @@ -13,98 +13,134 @@ Dialog - + - - - Name - - - - - - - MyNewStudy - - - - - - - Time system - - - - - - - + + + - Time - - - true + Name - - + + + + MyNewStudy + + + + + + + Time system + + + + + - + - Date + Time + + + true - - - Qt::Vertical - - - - - - - false - - - Staring date - - - - - - - false - - - dd/MM/yyyy HH:mm:ss - - + + + + + Date + + + + + + + Qt::Vertical + + + + + + + false + + + Staring date + + + + + + + false + + + dd/MM/yyyy HH:mm:ss + + + + + + + + Description + + + + + + + false + + + false + + + - - - - Description - - + + + + + + Creation date : + + + + + + + + + + + + + + Last modification : + + + + + + + + + + + - - - - false - - - false - - - - + Qt::Horizontal From 2f7843200525e86961e323dbdafd501ccf6cb427 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 16:14:44 +0200 Subject: [PATCH 035/289] pamhyr: Start switch to logging for program logs. --- src/Model/DB.py | 7 +++++-- src/View/Configure/Window.py | 7 ++++++- src/View/MainWindow.py | 19 +++++++++++++++++-- src/config.py | 9 ++++++--- src/pamhyr.py | 10 +++++++++- src/tools.py | 17 ++++++++++++----- 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/Model/DB.py b/src/Model/DB.py index 9405ef78..eb836c04 100644 --- a/src/Model/DB.py +++ b/src/Model/DB.py @@ -2,12 +2,15 @@ import os import sqlite3 +import logging from pathlib import Path from tools import SQL from Model.Except import NotImplementedMethodeError +logger = logging.getLogger() + # Top level model class class SQLModel(SQL): _sub_classes = [] @@ -22,11 +25,11 @@ class SQLModel(SQL): self._cur = self._db.cursor() if is_new: - print("CREATE") + logger.info("Create database") self._create() # Create db # self._save() # Save else: - print("UPDATE") + logger.info("Update database") self._update() # Update db scheme if necessary # self._load() # Load data diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index 69d64a69..818e5d9c 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- +import logging + from copy import deepcopy + from config import Config from View.ASubWindow import ASubWindow from View.ListedSubWindow import ListedSubWindow @@ -27,6 +30,8 @@ from PyQt5.QtWidgets import ( from Solver.Solvers import solver_long_name +logger = logging.getLogger() + class SolverTableModel(QAbstractTableModel): def __init__(self, headers=[], rows=[]): super(QAbstractTableModel, self).__init__() @@ -215,7 +220,7 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): # Debug def set_debug(self): self.conf.debug = not self.conf.debug - print(f"[DEBUG] Debug mode set : {self.conf.debug}") + logger.info(f"Debug mode set : {self.conf.debug}") self.parent.setup_debug_mode() # Solvers diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index e763e956..d44e53f0 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import logging from queue import Queue from PyQt5 import QtGui @@ -39,6 +40,8 @@ from View.Debug.Window import ReplWindow from Model.Study import Study +logger = logging.getLogger() + no_model_action = [ "action_menu_new", "action_menu_open", "action_menu_import_mage", "action_menu_import_rubarbe", "action_toolBar_open", @@ -206,6 +209,15 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # Maximise window self.showMaximized() + def set_debug_lvl(self, debug = True): + if debug: + logger.setLevel(logging.DEBUG) + logger.info("Set logging level to DEBUG") + else: + logger.setLevel(logging.INFO) + logger.info("Set logging level to INFO") + + def setup_debug_mode(self, init = False): menu = self.findChild(QMenu, "menu_help") @@ -216,11 +228,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self.conf.debug: menu.addAction(self.debug_action) + self.set_debug_lvl(debug = True) else: if self.conf.debug: menu.addAction(self.debug_action) + self.set_debug_lvl(debug = True) else: menu.removeAction(self.debug_action) + self.set_debug_lvl(debug = False) ######### # MODEL # @@ -283,7 +298,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): else: self.model.filename = file_name + ".pamhyr" - print("[INFO] Save...") + logger.info("Save...") self.model.save() def save_as_study(self): @@ -380,7 +395,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if dialog.exec_(): file_name = dialog.selectedFiles() self.set_model(Study.open(file_name[0])) - print(f"[MainWindow] Open Study : {self.model.name}") + logger.info(f"Open Study - {self.model.name}") def open_new_study(self): """Open dialog to set new study diff --git a/src/config.py b/src/config.py index 24ebebe9..c865ded2 100644 --- a/src/config.py +++ b/src/config.py @@ -2,6 +2,7 @@ import os import pickle +import logging from tools import SQL @@ -13,13 +14,15 @@ from Solver.Solvers import solver_type_list config_dir = "/.cache/pamhyr/" config_file = "config.sqlite3" +logger = logging.getLogger() + class Config(SQL): def __init__(self): self._version = '0.0.1' self.filename = Config.filename() self.set_default_value() - print(self.filename) + logging.info(f"Configuration file : {self.filename}") super(Config, self).__init__(filename = self.filename) @@ -61,10 +64,10 @@ class Config(SQL): self.commit() def _update(self): - version = self.execute(f"SELECT value FROM info WHERE key='version'") + version = self.execute(f"SELECT value FROM info WHERE key='version'")[0] if version != self._version: - print("update") + logger.info(f"Configuration file update from {version} to {self._version}...") def _load_solver(self): self._solvers = [] diff --git a/src/pamhyr.py b/src/pamhyr.py index 6bfa2f9b..8a832e18 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -3,6 +3,7 @@ import sys, os import locale +import logging from PyQt5.QtCore import QTranslator from PyQt5.QtWidgets import QApplication @@ -15,6 +16,13 @@ from tools import ( from View.MainWindow import ApplicationWindow from Model.Study import Study +logging.basicConfig( + level=logging.DEBUG, + format='[PAMHYR][%(levelname)s] %(message)s' +) +logger = logging.getLogger() +logger.setLevel(logging.INFO) + def main(): conf = Config.load() app = QApplication(sys.argv) @@ -37,7 +45,7 @@ def main(): if lang_file != "": ok = translator.load(lang_file) if not ok: - print("failed") + logger.error("Failed to load translate file") app.installTranslator(translator) diff --git a/src/tools.py b/src/tools.py index 410cb799..701dd236 100644 --- a/src/tools.py +++ b/src/tools.py @@ -3,6 +3,7 @@ import os import time import sqlite3 +import logging import traceback from datetime import datetime @@ -16,6 +17,12 @@ from functools import ( reduce, partial, wraps ) +########### +# LOGGING # +########### + +logger = logging.getLogger() + ########## # TIMERS # ########## @@ -242,7 +249,7 @@ class SQL(object): @timer def execute(self, cmd, fetch_one = True, commit = False): - # print(f"[SQL] {cmd}") + logger.debug(f"SQL - {cmd}") res = self._cur.execute(cmd) if commit: @@ -252,13 +259,13 @@ class SQL(object): return value def _create(self): - print("TODO: Create") + logger.warning("TODO: Create") def _update(self): - print("TODO: Update") + logger.warning("TODO: Update") def _save(self): - print("TODO: Save") + logger.warning("TODO: Save") def _load(self): - print("TODO: Load") + logger.warning("TODO: LOAD") From 0a414f4065912c49e1ed758fb13bbade4e5814ef Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 16:26:11 +0200 Subject: [PATCH 036/289] Solver: Switch to logging. --- src/Solver/ASolver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index 3b8419c7..e94bac4b 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import logging try: from signal import SIGTERM, SIGSTOP, SIGCONT @@ -12,6 +13,8 @@ from enum import Enum from Model.Except import NotImplementedMethodeError +logger = logging.getLogger() + class STATUS(Enum): NOT_LAUNCHED = -1 STOPED = 0 @@ -159,7 +162,7 @@ class AbstractSolver(object): cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) - print(f"! {cmd}") + logger.debug(f"! {cmd}") cmd = cmd.split() exe = cmd[0] @@ -172,7 +175,6 @@ class AbstractSolver(object): return True def run_solver(self): - print("run solver") if self._cmd_solver == "": self._run_next() return True @@ -182,7 +184,7 @@ class AbstractSolver(object): cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) - print(f"! {cmd}") + logger.debug(f"! {cmd}") cmd = cmd.split() exe = cmd[0] @@ -205,7 +207,7 @@ class AbstractSolver(object): cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) - print(f"! {cmd}") + logger.debug(f"! {cmd}") cmd = cmd.split() exe = cmd[0] From bc78542d2c47bba7db5092c862b40f2039e57767 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 16:28:43 +0200 Subject: [PATCH 037/289] Study: Switch to logging. --- src/Model/Study.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Model/Study.py b/src/Model/Study.py index ff806264..c1005840 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -2,6 +2,7 @@ import os import shutil +import logging from datetime import datetime from tools import timer, timestamp @@ -14,6 +15,8 @@ from Model.River import River from Checker.Study import * +logger = logging.getLogger() + class Study(SQLModel): _sub_classes = [ River, @@ -81,7 +84,7 @@ class Study(SQLModel): shutil.rmtree(old_dir) os.makedirs(old_dir) except Exception as e: - print(e) + logger.error(e) fname = fname + "." + str(self._old_save_id) filename = os.path.join(fdir, "__old__", fname) @@ -187,7 +190,7 @@ class Study(SQLModel): def _update(self): version = self.execute(f"SELECT value FROM info WHERE key='version'") - print(f"{version[0]} == {self._version}") + logger.debug(f"{version[0]} == {self._version}") if version[0] == self._version: return True @@ -195,7 +198,7 @@ class Study(SQLModel): self.execute(f"UPDATE info SET value='{self._version}' WHERE key='version'") return True - print("TODO: update failed") + logger.info("TODO: update failed") raise NotImplementedMethodeError(self, self._update) @classmethod From e925a9598717e7d9273b5060f81519b026949d17 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 16:49:26 +0200 Subject: [PATCH 038/289] Model, View: Some switch to logging. --- src/Model/Geometry/Profile.py | 6 +++++- src/Model/Geometry/Reach.py | 9 ++++++--- src/View/ASubWindow.py | 5 ++++- src/View/CheckList/Window.py | 1 - src/View/Debug/Window.py | 6 +++++- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 4a1f5a5f..b0328f80 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- +import logging + from tools import timer from Model.Geometry.Point import Point from Model.Except import NotImplementedMethodeError +logger = logging.getLogger() + class Profile(object): _id_cnt = 0 @@ -241,7 +245,7 @@ class Profile(object): @timer def sort_with_indexes(self, indexes: list): if len(self._points) != len(indexes): - print("TODO: CRITICAL ERROR!") + logger.critical("Indexes list do not correspond to point list") self._points = list( map( diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index fa72c8ec..1f429be7 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import logging import numpy as np from time import time @@ -17,6 +18,8 @@ from Model.Geometry.ProfileXYZ import ProfileXYZ from Model.Except import FileFormatError, exception_message_box +logger = logging.getLogger() + class Reach(SQLSubModel): _sub_classes = [ ProfileXYZ, @@ -377,7 +380,7 @@ class Reach(SQLSubModel): @timer def sort_with_indexes(self, indexes: list): if len(self._profiles) != len(indexes): - print("TODO: CRITICAL ERROR!") + logger.critical("Indexes list do not correspond to profile list") self._profiles = list( map( @@ -424,10 +427,10 @@ class Reach(SQLSubModel): self._status.modified() except FileNotFoundError as e: - print(e) + logger.error(e) exception_message_box(e) except FileFormatError as e: - print(e) + logger.error(e) e.alert() @timer diff --git a/src/View/ASubWindow.py b/src/View/ASubWindow.py index 5bf0940c..13719d77 100644 --- a/src/View/ASubWindow.py +++ b/src/View/ASubWindow.py @@ -2,6 +2,7 @@ import os import csv +import logging from io import StringIO from datetime import datetime @@ -26,6 +27,8 @@ from PyQt5.uic import loadUi from Model.Except import ClipboardFormatError +logger = logging.getLogger() + class WindowToolKit(object): def __init__(self, parent=None): super(WindowToolKit, self).__init__() @@ -140,7 +143,7 @@ class ASubWindowFeatures(object): try: self.find(QLineEdit, name).setText(text) except AttributeError as e: - print(e) + logger.error(e) def get_line_edit_text(self, name:str): """Get text of line edit component diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index 8f720e0f..d04c8913 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -169,7 +169,6 @@ class CheckListWindow(ASubMainWindow, ListedSubWindow): self.close() def reject(self): - print("cancel") self.end() def accept(self): diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py index 5fad77df..c02075a7 100644 --- a/src/View/Debug/Window.py +++ b/src/View/Debug/Window.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from View.ASubWindow import ASubMainWindow @@ -23,6 +25,8 @@ from PyQt5.QtWidgets import ( QProgressBar, QLabel, QTextEdit, ) +logger = logging.getLogger() + _translate = QCoreApplication.translate class ReplWindow(ASubMainWindow, ListedSubWindow): @@ -71,7 +75,7 @@ class ReplWindow(ASubMainWindow, ListedSubWindow): self._history_ind = 0 rich_code = "self.__debug_exec_result__ = " + code - print(f"[DEBUG] ! {code}") + logger.debug(f"{code}") try: value = exec(rich_code) value = self.__debug_exec_result__ From c1b3d42a6affab94a948fd35cb88054a9a2bfe6e Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 5 Jul 2023 16:53:33 +0200 Subject: [PATCH 039/289] ListedSubWindow: Switch to logging. --- src/View/ListedSubWindow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/View/ListedSubWindow.py b/src/View/ListedSubWindow.py index 719de3d6..c88fd7b4 100644 --- a/src/View/ListedSubWindow.py +++ b/src/View/ListedSubWindow.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +import logging from functools import reduce +logger = logging.getLogger() + class ListedSubWindow(object): def __init__(self): super(ListedSubWindow, self).__init__() @@ -17,7 +20,7 @@ class ListedSubWindow(object): def sub_win_add(self, name, win): self.sub_win_list.append((name, win)) self.sub_win_cnt += 1 - print(f"+ {name} ({self.sub_win_cnt})") + logger.info(f"Open window: {name} ({self.sub_win_cnt})") def sub_win_del(self, name): self.sub_win_list = list( @@ -27,7 +30,7 @@ class ListedSubWindow(object): ) ) self.sub_win_cnt = len(self.sub_win_list) - print(f"- {name} ({self.sub_win_cnt})") + logger.info(f"Close window: {name} ({self.sub_win_cnt})") def sub_win_exists(self, name): return reduce( From a8bd871f9452da39f470a6b2ff811a3e9b0c9c1f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Jul 2023 09:58:55 +0200 Subject: [PATCH 040/289] IC: Minor change. --- src/View/InitialConditions/translate.py | 6 +++--- src/View/ui/InitialConditions.ui | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/View/InitialConditions/translate.py b/src/View/InitialConditions/translate.py index 18090daf..43890acb 100644 --- a/src/View/InitialConditions/translate.py +++ b/src/View/InitialConditions/translate.py @@ -6,10 +6,10 @@ _translate = QCoreApplication.translate table_headers = { "name": _translate("LateralContribution", "Name"), - "comment": _translate("LateralContribution", "Comment"), "kp": _translate("LateralContribution", "KP (m)"), - "speed": _translate("LateralContribution", "Speed (m/s)"), + # "speed": _translate("LateralContribution", "Speed (m/s)"), "discharge": _translate("LateralContribution", "Discharge (m³/s)"), "elevation": _translate("LateralContribution", "Elevation (m)"), - "height": _translate("LateralContribution", "Height (m)") + "height": _translate("LateralContribution", "Height (m)"), + "comment": _translate("LateralContribution", "Comment"), } diff --git a/src/View/ui/InitialConditions.ui b/src/View/ui/InitialConditions.ui index 087dbe02..f84d1f3d 100644 --- a/src/View/ui/InitialConditions.ui +++ b/src/View/ui/InitialConditions.ui @@ -20,21 +20,21 @@ Qt::Horizontal - + - Generate 1 + Generate minimal height - Generate 2 + Generate constant discharge From 504ef7ec4820d6184c6fb006b7f54ed79392716c Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Jul 2023 10:50:17 +0200 Subject: [PATCH 041/289] pamhyr: End switch print to logging. --- src/View/BoundaryCondition/Edit/Table.py | 7 +++++-- src/View/BoundaryCondition/Window.py | 7 +++++-- src/View/Geometry/PlotAC.py | 6 +++++- src/View/Geometry/PlotKPC.py | 6 +++++- src/View/Geometry/Profile/Plot.py | 6 +++++- src/View/Geometry/Window.py | 4 ++-- src/View/InitialConditions/Window.py | 7 +++++-- src/View/LateralContribution/Edit/Table.py | 6 ++++-- src/View/LateralContribution/Window.py | 7 +++++-- src/View/Plot/mpl_canvas_onpick_event.py | 8 +++++--- src/View/Plot/navigation_toolbar_2qt.py | 4 +++- src/View/Sections/Window.py | 8 ++++++-- src/View/SolverParameters/Window.py | 7 +++++-- src/View/Stricklers/Window.py | 7 +++++-- src/tools.py | 12 ++++++------ 15 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 6c94b928..dfe805e1 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from datetime import date, time, datetime, timedelta from tools import trace, timer @@ -32,6 +34,7 @@ from View.BoundaryCondition.Edit.translate import * _translate = QCoreApplication.translate +logger = logging.getLogger() class ExtendedTimeEdit(AWidget): def __init__(self, parent=None): @@ -108,7 +111,7 @@ class ExTimeDelegate(QItemDelegate): self.editor = ExtendedDateTimeEdit(parent=parent) value = index.data(Qt.DisplayRole) self.editor.set_time(value) - print(value) + logger.debug(str(value)) return self.editor def setModelData(self, editor, model, index): @@ -116,7 +119,7 @@ class ExTimeDelegate(QItemDelegate): if self._mode == "time": model.setData(index, int(time.total_seconds())) else: - print(time.timestamp()) + logger.debug(str(time.timestamp())) model.setData(index, int(time.timestamp())) editor.close() editor.deleteLater() diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index 62d823d7..2e7dd430 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from View.ASubWindow import ASubMainWindow @@ -43,6 +45,7 @@ from View.BoundaryCondition.Edit.Window import EditBoundaryConditionWindow _translate = QCoreApplication.translate +logger = logging.getLogger() class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Boundary conditions", study=None, parent=None): @@ -184,10 +187,10 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self._table[tab].move_down(row) def copy(self): - print("TODO") + logger.info("TODO: copy") def paste(self): - print("TODO") + logger.info("TODO: paste") def undo(self): tab = self.current_tab() diff --git a/src/View/Geometry/PlotAC.py b/src/View/Geometry/PlotAC.py index da5f1149..9709d3bc 100644 --- a/src/View/Geometry/PlotAC.py +++ b/src/View/Geometry/PlotAC.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import timer from View.Plot.APlot import APlot @@ -9,6 +11,8 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate +logger = logging.getLogger() + class PlotAC(APlot): def __init__(self, canvas=None, data=None, toolbar=None, plot_xy=None): super(PlotAC, self).__init__( @@ -284,7 +288,7 @@ class PlotAC(APlot): self.color_incomplete_gl[lincomplete.index(txt)] ) except Exception as e: - print(f"{e}") + logger.warning(f"{e}") self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Geometry/PlotKPC.py b/src/View/Geometry/PlotKPC.py index d48381c4..f3e883f3 100644 --- a/src/View/Geometry/PlotKPC.py +++ b/src/View/Geometry/PlotKPC.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import timer from View.Plot.APlot import APlot @@ -9,6 +11,8 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate +logger = logging.getLogger() + class PlotKPC(APlot): def __init__(self, canvas=None, data=None, toolbar=None, display_current=True): @@ -214,7 +218,7 @@ class PlotKPC(APlot): kp, z_complete[i] ) except Exception as e: - print(f"Failed to update graphic 2: {e}") + logger.warning(f"Failed to update graphic KPC: {e}") self.canvas.axes.autoscale_view(True, True, True) self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Geometry/Profile/Plot.py b/src/View/Geometry/Profile/Plot.py index 100426f8..2dc7f522 100644 --- a/src/View/Geometry/Profile/Plot.py +++ b/src/View/Geometry/Profile/Plot.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import timer, trace from View.Plot.APlot import APlot from View.Plot.mpl_canvas_onpick_event import OnpickEvent @@ -10,6 +12,8 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate +logger = logging.getLogger() + class Plot(APlot): def __init__(self, canvas=None, data=None, toolbar=None, table=None): super(Plot, self).__init__( @@ -123,5 +127,5 @@ class Plot(APlot): @timer def update(self, ind=None): - print("TODO: implemente update") + logger.info("TODO: implemente update") self.draw() diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 0428a006..37f83aff 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -150,8 +150,8 @@ class GeometryWindow(QMainWindow, WindowToolKit): msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) return_value = msg_box.exec() - if return_value == QtWidgets.QMessageBox.Ok: - print('OK clicked') + # if return_value == QtWidgets.QMessageBox.Ok: + # print('OK clicked') def edit_profile(self): self.tableView.model().blockSignals(True) diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 22cade03..35822811 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from View.ASubWindow import ASubMainWindow @@ -39,6 +41,7 @@ from View.InitialConditions.DialogDischarge import DischargeDialog _translate = QCoreApplication.translate +logger = logging.getLogger() class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Initial condition", @@ -197,11 +200,11 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self._update_plot() def copy(self): - print("TODO") + logger.info("TODO: copy") self._update_plot() def paste(self): - print("TODO") + logger.info("TODO: paste") self._update_plot() def undo(self): diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index e3e74483..4101995c 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import logging from datetime import date, time, datetime, timedelta from tools import trace, timer @@ -31,6 +32,7 @@ from View.LateralContribution.Edit.translate import * _translate = QCoreApplication.translate +logger = logging.getLogger() class ExtendedTimeEdit(AWidget): def __init__(self, parent=None): @@ -107,7 +109,7 @@ class ExTimeDelegate(QItemDelegate): self.editor = ExtendedDateTimeEdit(parent=parent) value = index.data(Qt.DisplayRole) self.editor.set_time(value) - print(value) + logger.debug(str(value)) return self.editor def setModelData(self, editor, model, index): @@ -115,7 +117,7 @@ class ExTimeDelegate(QItemDelegate): if self._mode == "time": model.setData(index, int(time.total_seconds())) else: - print(time.timestamp()) + logger.debug(str(time.timestamp())) model.setData(index, int(time.timestamp())) editor.close() editor.deleteLater() diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index a6b7b03e..b87e9968 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from View.ASubWindow import ASubMainWindow @@ -43,6 +45,7 @@ from View.LateralContribution.Edit.Window import EditLateralContributionWindow _translate = QCoreApplication.translate +logger = logging.getLogger() class LateralContributionWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Lateral contribution", study=None, parent=None): @@ -226,11 +229,11 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self._set_current_reach() def copy(self): - print("TODO") + logger.info("TODO: copy") self._set_current_reach() def paste(self): - print("TODO") + logger.info("TODO: paste") self._set_current_reach() def undo(self): diff --git a/src/View/Plot/mpl_canvas_onpick_event.py b/src/View/Plot/mpl_canvas_onpick_event.py index 6415aee7..010959bc 100644 --- a/src/View/Plot/mpl_canvas_onpick_event.py +++ b/src/View/Plot/mpl_canvas_onpick_event.py @@ -1,3 +1,5 @@ +import logging + from time import time import numpy as np @@ -6,6 +8,7 @@ from PyQt5.QtCore import QItemSelectionModel, Qt from PyQt5.QtWidgets import QApplication 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): @@ -105,14 +108,13 @@ class OnpickEvent(object): [pl[0].set_data([], []) for pl in self.pt if len(self.pt) > 1] except: - print("Probleme de mise à jour ... update_select_point_point()") + logger.info("update_select_point_point: Update issue") try: self.update_select_point_point_bis(self.x[row], self.y[row]) except: - print("index introuvable pour la mise à jour de l'affichage de la sélection du point." - "Editer les cases en 'nan'.") + logger.info("update_select_point_point_bis: Update issue, possible index missing") self.ax.figure.canvas.draw_idle() diff --git a/src/View/Plot/navigation_toolbar_2qt.py b/src/View/Plot/navigation_toolbar_2qt.py index d07386de..37186450 100644 --- a/src/View/Plot/navigation_toolbar_2qt.py +++ b/src/View/Plot/navigation_toolbar_2qt.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import logging import matplotlib as mpl from matplotlib.backends import qt_compat from PyQt5 import QtCore, QtGui, QtWidgets @@ -10,6 +11,7 @@ from PyQt5.QtCore import pyqtSignal, QSize _translate = QtCore.QCoreApplication.translate +logger = logging.getLogger() file_path = os.path.abspath(os.path.dirname(__file__)) @@ -123,7 +125,7 @@ class PamHyrNavigationToolbar2QT(NavigationToolbar2QT): ) def cursor1(self): - print("TODO") + logger.info("TODO: cursor1") def isometric_view(self): self.my_canvas.axes.axis("equal") diff --git a/src/View/Sections/Window.py b/src/View/Sections/Window.py index 5690b472..63eb4b7e 100644 --- a/src/View/Sections/Window.py +++ b/src/View/Sections/Window.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from View.ASubWindow import ASubMainWindow @@ -39,6 +41,8 @@ from View.Stricklers.Window import StricklersWindow _translate = QCoreApplication.translate +logger = logging.getLogger() + class SectionsWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Sections", study=None, parent=None): self._study = study @@ -214,10 +218,10 @@ class SectionsWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(row) def copy(self): - print("TODO") + logger.info("TODO: copy") def paste(self): - print("TODO") + logger.info("TODO: paste") def undo(self): self._table.undo() diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index e2ceae2b..4e154a54 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from View.ASubWindow import ASubMainWindow @@ -31,6 +33,7 @@ from Solver.Solvers import solver_long_name, solver_type_list _translate = QCoreApplication.translate +logger = logging.getLogger() class SolverParametersWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Solver parameters", study=None, parent=None): @@ -120,7 +123,7 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): self._set_current_reach() def copy(self): - print("TODO") + logger.info("TODO: copy") def paste(self): - print("TODO") + logger.info("TODO: paste") diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index be02f4e7..1d2bfe43 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer from View.ASubWindow import ASubMainWindow @@ -29,6 +31,7 @@ from View.Stricklers.Table import TableModel _translate = QCoreApplication.translate +logger = logging.getLogger() class StricklersWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Stricklers", study=None, config=None, parent=None): @@ -115,10 +118,10 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): self._table['study'].sort(False) def copy(self): - print("TODO") + logger.info("TODO: copy") def paste(self): - print("TODO") + logger.info("TODO: paste") def undo(self): self._table['study'].undo() diff --git a/src/tools.py b/src/tools.py index 701dd236..0e7a48d6 100644 --- a/src/tools.py +++ b/src/tools.py @@ -53,7 +53,7 @@ def display_timers(): for t in range(fmax + 26): head += "-" head += "+" - print(head) + logger.debug(head) lst = sorted( map( @@ -67,13 +67,13 @@ def display_timers(): for func, time, calls in lst: name = (f"{Fore.BLUE}{func.__module__}{Style.RESET_ALL}" + f".{Style.BRIGHT}{Fore.GREEN}{func.__qualname__:<{fmax - len(func.__module__)}}{Style.RESET_ALL}") - print(f" | {name} | {time:>10.6f} sec | {calls:>5} calls |") + logger.debug(f" | {name} | {time:>10.6f} sec | {calls:>5} calls |") tail = " +--" for t in range(fmax + 32): tail += "-" tail += "+" - print(tail) + logger.debug(tail) def timer(func): """Function wrapper to register function runtime""" @@ -85,7 +85,7 @@ def timer(func): try: value = func(*args, **kwargs) except Exception as e: - print(f"[{Fore.RED}ERROR{Style.RESET_ALL}]" + + logger.debug(f"[{Fore.RED}ERROR{Style.RESET_ALL}]" + f"[{func.__module__}.{Fore.GREEN}{func.__qualname__}{Style.RESET_ALL}]: " + f"{Fore.RED}{e}{Style.RESET_ALL}") traceback.print_exc() @@ -113,13 +113,13 @@ def trace(func): t = time.ctime() head = f"[{Fore.BLUE}TRACE{Style.RESET_ALL}]" c = f"{head}[{t}] Call {func.__module__}.{Fore.GREEN}{func.__qualname__}{Style.RESET_ALL}({args}, {kwargs})" - print(c) + logger.debug(c) value = func(*args, **kwargs) t = time.ctime() r = f"{head}[{t}] Return {func.__module__}.{Fore.GREEN}{func.__qualname__}{Style.RESET_ALL}: {value}" - print(r) + logger.debug(r) return value From 609e9d39db3ec1ea5651eb58b94f01bdc22a4fee Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Jul 2023 11:36:06 +0200 Subject: [PATCH 042/289] Model: Minor change. --- src/Model/Saved.py | 8 ++++++-- src/Model/Study.py | 4 ++-- src/View/MainWindow.py | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Model/Saved.py b/src/Model/Saved.py index b11ed471..3f672731 100644 --- a/src/Model/Saved.py +++ b/src/Model/Saved.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- +import logging + +logger = logging.getLogger() + class SavedStatus(object): def __init__(self): super(SavedStatus, self).__init__() @@ -9,9 +13,9 @@ class SavedStatus(object): return self._saved def save(self): - # print(" * save") + logger.debug("model status set as saved") self._saved = True def modified(self): - # print(" * modified ...") + logger.debug("model status set as modified") self._saved = False diff --git a/src/Model/Study.py b/src/Model/Study.py index c1005840..413838ad 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -74,7 +74,7 @@ class Study(SQLModel): fdir, fname = os.path.split(self.filename) if self._old_save_id == 0: - old_dir = os.path.join(fdir, "__old__") + old_dir = os.path.join(fdir, "_PAMHYR_", "__old__") if os.name == "nt": old_dir = old_dir.replace("/", "\\") @@ -87,7 +87,7 @@ class Study(SQLModel): logger.error(e) fname = fname + "." + str(self._old_save_id) - filename = os.path.join(fdir, "__old__", fname) + filename = os.path.join(fdir, "_PAMHYR_", "__old__", fname) shutil.copy(self.filename, filename) self._old_save_id += 1 diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index d44e53f0..52dba9fc 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -298,6 +298,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): else: self.model.filename = file_name + ".pamhyr" + if self.model.is_saved: + return + logger.info("Save...") self.model.save() From 17b11b34a72b042639cc63d6be06ed70e2c1f6d8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Jul 2023 11:36:21 +0200 Subject: [PATCH 043/289] Debug: Switch to multiline debug command. --- src/View/ASubWindow.py | 25 ++++++++++++++++++++++++- src/View/Debug/Window.py | 20 ++++++++++++-------- src/View/ui/DebugRepl.ui | 2 +- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/View/ASubWindow.py b/src/View/ASubWindow.py index 13719d77..478993cf 100644 --- a/src/View/ASubWindow.py +++ b/src/View/ASubWindow.py @@ -18,7 +18,7 @@ from PyQt5.QtWidgets import ( QTimeEdit, QSpinBox, QTextEdit, QRadioButton, QComboBox, QFileDialog, QMessageBox, QTableView, QAction, - QDateTimeEdit, QWidget, + QDateTimeEdit, QWidget, QPlainTextEdit, ) from PyQt5.QtCore import ( QTime, QDateTime, @@ -179,6 +179,29 @@ class ASubWindowFeatures(object): """ return self.find(QTextEdit, name).toHtml() + def set_plaintext_edit_text(self, name:str, text:str): + """Set text of text edit component + + Args: + text_edit: The text edit component name + text: The text + + Returns: + Nothing + """ + self.find(QPlainTextEdit, name).setPlainText(text) + + def get_plaintext_edit_text(self, name:str): + """Get text of text edit component + + Args: + text_edit: The text edit component name + + Returns: + Text + """ + return self.find(QPlainTextEdit, name).toPlainText() + def set_check_box(self, name:str, checked:bool): """Set status of checkbox component diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py index c02075a7..974eace3 100644 --- a/src/View/Debug/Window.py +++ b/src/View/Debug/Window.py @@ -51,8 +51,8 @@ class ReplWindow(ASubMainWindow, ListedSubWindow): self.setup_connections() def setup_connections(self): - self._hup_sc = QShortcut(QKeySequence("Up"), self) - self._hdown_sc = QShortcut(QKeySequence("Down"), self) + self._hup_sc = QShortcut(QKeySequence("ctrl+Up"), self) + self._hdown_sc = QShortcut(QKeySequence("ctrl+Down"), self) self._hup_sc.activated.connect(self.history_up) self._hdown_sc.activated.connect(self.history_down) @@ -61,21 +61,25 @@ class ReplWindow(ASubMainWindow, ListedSubWindow): def history_up(self): if self._history_ind < len(self._history): self._history_ind += 1 - self.set_line_edit_text("lineEdit", 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_line_edit_text("lineEdit", self._history[-self._history_ind]) + self.set_plaintext_edit_text("plainTextEdit", self._history[-self._history_ind]) + else: + self.set_plaintext_edit_text("plainTextEdit", "") def eval_python(self): - code = self.get_line_edit_text("lineEdit") + code = self.get_plaintext_edit_text("plainTextEdit") self._history.append(code) - self.set_line_edit_text("lineEdit", "") + self.set_plaintext_edit_text("plainTextEdit", "") self._history_ind = 0 - rich_code = "self.__debug_exec_result__ = " + code - logger.debug(f"{code}") + rich_code = code.strip().split("\n") + rich_code[-1] = "self.__debug_exec_result__ = " + rich_code[-1] + rich_code = "\n".join(rich_code) + logger.debug(f"User debug command : {rich_code}") try: value = exec(rich_code) value = self.__debug_exec_result__ diff --git a/src/View/ui/DebugRepl.ui b/src/View/ui/DebugRepl.ui index f889061d..03494a0c 100644 --- a/src/View/ui/DebugRepl.ui +++ b/src/View/ui/DebugRepl.ui @@ -21,7 +21,7 @@ - + From 53dc068dabd2fd63e0963d72792072a75375d3b5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Jul 2023 14:14:11 +0200 Subject: [PATCH 044/289] MainWindow: Minor change. --- src/View/MainWindow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 52dba9fc..024f611f 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -399,6 +399,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): file_name = dialog.selectedFiles() self.set_model(Study.open(file_name[0])) logger.info(f"Open Study - {self.model.name}") + self.set_title() def open_new_study(self): """Open dialog to set new study From c2c1667a7356837af331eb2757b2edc3427fbb36 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Jul 2023 14:45:34 +0200 Subject: [PATCH 045/289] Network: If there is only one edge, select it as current reach. --- src/Model/Network/Graph.py | 15 +++++++++++++++ src/Model/River.py | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/src/Model/Network/Graph.py b/src/Model/Network/Graph.py index 0f432495..e2d549fb 100644 --- a/src/Model/Network/Graph.py +++ b/src/Model/Network/Graph.py @@ -29,6 +29,14 @@ class Graph(object): def edges(self): return self._edges + def enable_edges(self): + return list( + filter( + lambda e: e.is_enable(), + self._edges + ) + ) + def edges_names(self): return list(map(lambda e: e.name, self._edges)) @@ -38,6 +46,13 @@ class Graph(object): def edges_counts(self): return len(self._edges) + def enable_edges_counts(self): + return reduce( + lambda acc, e: acc + 1 if e.is_enable() else acc, + self._edges, + 0 + ) + def is_node_exists(self, node_name): return reduce( lambda acc, n: (acc or (n.name == node_name)), diff --git a/src/Model/River.py b/src/Model/River.py index 9f5505ad..cbc86f19 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -336,9 +336,16 @@ class River(Graph, SQLSubModel): return self._parameters[solver] def has_current_reach(self): + if self.enable_edges_counts() == 1: + return True + return self._current_reach is not None def current_reach(self): + ee = self.enable_edges() + if len(ee) == 1: + return ee[0] + return self._current_reach def set_current_reach(self, reach): From 651061ede0bf237dd4fd9d5a138f9a404a4c3ccb Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Jul 2023 16:46:46 +0200 Subject: [PATCH 046/289] Model, Solver: Some minor fix for multi reach study. --- src/Model/Geometry/Reach.py | 4 +-- src/Solver/Mage.py | 61 ++++++++++++++++++++++--------------- src/View/Geometry/Window.py | 7 ++--- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 1f429be7..b18f2ade 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -55,8 +55,8 @@ class Reach(SQLSubModel): def _sql_save(self, execute, data = None): # Delete old data - execute(f"DELETE FROM geometry_profileXYZ") - execute(f"DELETE FROM geometry_pointXYZ") + execute(f"DELETE FROM geometry_profileXYZ WHERE reach = {self.id}") + # execute(f"DELETE FROM geometry_pointXYZ") if data is None: data = {} diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 78a89931..eedd1215 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -119,18 +119,11 @@ class Mage(AbstractSolver): return files @timer - def _export_BC(self, bound, repertory, qlog): + def _export_BC(self, t, bounds, repertory, qlog): files = [] - t = "XX" - if bound.bctype == "ZD": - t = "AVA" - elif bound.bctype == "TD": - t = "HYD" - elif bound.bctype == "TZ": - t = "LIM" - else: - return None + if len(bounds) == 0: + return files if qlog is not None: qlog.put(f"Export {t} file") @@ -140,14 +133,15 @@ class Mage(AbstractSolver): f.write("* This file is generate by PAMHYR, please don't modify\n") - name = f"{bound.node.id:3}".replace(" ", "x") - f.write(f"* {bound.node.name} ({name}) {bound.bctype}\n") - f.write(f"${name}\n") - header = bound.header - f.write(f"*{header[0]:>9}|{header[1]:>10}\n") + for bound in bounds: + name = f"{bound.node.id:3}".replace(" ", "x") + f.write(f"* {bound.node.name} ({name}) {bound.bctype}\n") + f.write(f"${name}\n") + header = bound.header + f.write(f"*{header[0]:>9}|{header[1]:>10}\n") - for d in bound.data: - f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n") + for d in bound.data: + f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n") return files @@ -156,9 +150,22 @@ class Mage(AbstractSolver): files = [] lst = study.river.boundary_condition + AVA = [] + HYD = [] + LIM = [] + for tab in ["liquid", "solid", "suspenssion"]: for bound in lst.get_tab(tab): - files = files + self._export_BC(bound, repertory, qlog) + if bound.bctype == "ZD": + AVA.append(bound) + elif bound.bctype == "TD" or bound.bctype == "PC": + HYD.append(bound) + elif bound.bctype == "TZ": + LIM.append(bound) + + files = files + self._export_BC("AVA", AVA, repertory, qlog) + files = files + self._export_BC("HYD", HYD, repertory, qlog) + files = files + self._export_BC("LIM", LIM, repertory, qlog) return files @@ -247,12 +254,7 @@ class Mage(AbstractSolver): # Write header with open(os.path.join(repertory, "0.INI"), "w+") as f: - files.append("0.INI") - f.write("* This file is generate by PAMHYR, please don't modify\n") - # TODO put real date... - f.write(f"$ date en minutes : 0.00\n") - f.write(f"* IB IS discharge elevation kp\n") - + has_ini = False id = 1 reachs = study.river.edges() reachs = list( @@ -262,10 +264,19 @@ class Mage(AbstractSolver): ) ) + f.write("* This file is generate by PAMHYR, please don't modify\n") + # TODO put real date... + f.write(f"$ date en minutes : 0.00\n") + f.write(f"* IB IS discharge elevation kp\n") + id = 1 for reach in reachs: cond = study.river.initial_conditions.get(reach) data = cond.data + if len(data) == 0: + continue + + has_ini = True id_sec = 1 for d in data: @@ -280,6 +291,8 @@ class Mage(AbstractSolver): id += 1 + if has_ini: + files.append("0.INI") return files @timer diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 37f83aff..8efea1c3 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -111,24 +111,21 @@ class GeometryWindow(QMainWindow, WindowToolKit): def open_file_dialog(self): options = QFileDialog.Options() - DEFAULT_DIRECTORY = os.getenv("HOME") settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) - current_dir = settings.value('current_directory', DEFAULT_DIRECTORY, type=str) options |= QFileDialog.DontUseNativeDialog filename, _ = QtWidgets.QFileDialog.getOpenFileName( self, _translate("MainWindow_reach", "Ouvrir un fichier"), - current_dir, + "", _translate("MainWindow_reach", "Fichiers .ST (*.ST)") + ";; " + - _translate("MainWindow_reach", "Fichiers textes (*.txt)") + + _translate("MainWindow_reach", "Fichiers .M (*.M)") + ";; " + _translate("MainWindow_reach", "Tous les fichiers (*)"), options=options ) - current_dir = os.path.split(filename)[0] or DEFAULT_DIRECTORY if filename != "": size = os.stat(filename).st_size self._reach.import_geometry(filename) From 1d3cf8bd945db261230a7d38b8ebe8e8186307a0 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 7 Jul 2023 16:47:21 +0200 Subject: [PATCH 047/289] tests_case: Add Ardeche study. --- tests_cases/Ardeche/Ardeche.pamhyr | Bin 0 -> 311296 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests_cases/Ardeche/Ardeche.pamhyr diff --git a/tests_cases/Ardeche/Ardeche.pamhyr b/tests_cases/Ardeche/Ardeche.pamhyr new file mode 100644 index 0000000000000000000000000000000000000000..1dc13ad2416f94f848fa368d441eb3b20921c77b GIT binary patch literal 311296 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 literal 0 HcmV?d00001 From 956a0714a2863d50554bd3cb1de88e14464fda57 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 17 Jul 2023 11:03:56 +0200 Subject: [PATCH 048/289] Friction: Rename 'Section' to 'Friction'. --- src/Model/DB.py | 1 - .../Section.py => Friction/Friction.py} | 12 +- .../FrictionList.py} | 66 ++--- src/Model/River.py | 20 +- src/Model/Study.py | 6 +- src/Solver/Mage.py | 12 +- .../{Sections => Frictions}/PlotStricklers.py | 16 +- src/View/{Sections => Frictions}/Table.py | 50 ++-- src/View/Frictions/UndoCommand.py | 226 ++++++++++++++++++ src/View/{Sections => Frictions}/Window.py | 24 +- src/View/Frictions/translate.py | 14 ++ src/View/MainWindow.py | 15 +- src/View/Sections/UndoCommand.py | 226 ------------------ src/View/Sections/translate.py | 14 -- src/View/ui/{Sections.ui => Frictions.ui} | 0 src/View/ui/MainWindow.ui | 8 +- 16 files changed, 362 insertions(+), 348 deletions(-) rename src/Model/{Section/Section.py => Friction/Friction.py} (93%) rename src/Model/{Section/SectionList.py => Friction/FrictionList.py} (54%) rename src/View/{Sections => Frictions}/PlotStricklers.py (88%) rename src/View/{Sections => Frictions}/Table.py (81%) create mode 100644 src/View/Frictions/UndoCommand.py rename src/View/{Sections => Frictions}/Window.py (90%) create mode 100644 src/View/Frictions/translate.py delete mode 100644 src/View/Sections/UndoCommand.py delete mode 100644 src/View/Sections/translate.py rename src/View/ui/{Sections.ui => Frictions.ui} (100%) diff --git a/src/Model/DB.py b/src/Model/DB.py index eb836c04..90869740 100644 --- a/src/Model/DB.py +++ b/src/Model/DB.py @@ -29,7 +29,6 @@ class SQLModel(SQL): self._create() # Create db # self._save() # Save else: - logger.info("Update database") self._update() # Update db scheme if necessary # self._load() # Load data diff --git a/src/Model/Section/Section.py b/src/Model/Friction/Friction.py similarity index 93% rename from src/Model/Section/Section.py rename to src/Model/Friction/Friction.py index 86bdff48..f7328dd4 100644 --- a/src/Model/Section/Section.py +++ b/src/Model/Friction/Friction.py @@ -4,9 +4,9 @@ from tools import trace, timer from Model.DB import SQLSubModel -class Section(SQLSubModel): +class Friction(SQLSubModel): def __init__(self, name:str = "", status = None): - super(Section, self).__init__() + super(Friction, self).__init__() self._status = status @@ -20,7 +20,7 @@ class Section(SQLSubModel): @classmethod def _sql_create(cls, execute): execute(""" - CREATE TABLE section( + CREATE TABLE friction( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ind INTERGER NOT NULL, begin_kp REAL NOT NULL, @@ -49,7 +49,7 @@ class Section(SQLSubModel): table = execute( "SELECT ind, begin_kp, end_kp, begin_strickler, end_strickler " + - f"FROM section WHERE reach = {reach.id}" + f"FROM friction WHERE reach = {reach.id}" ) for _ in table: @@ -61,7 +61,7 @@ class Section(SQLSubModel): bs = next(filter(lambda s: s.id == row[3], stricklers)) es = next(filter(lambda s: s.id == row[4], stricklers)) - # Create section + # Create friction sec = cls(status = status) sec.edge = reach sec.begin_kp = row[1] @@ -77,7 +77,7 @@ class Section(SQLSubModel): ind = data["ind"] execute( "INSERT INTO " + - "section(ind, begin_kp, end_kp, reach, begin_strickler, end_strickler) " + + "friction(ind, begin_kp, end_kp, reach, begin_strickler, end_strickler) " + "VALUES (" + f"{ind}, {self._begin_kp}, {self._end_kp}, " + f"{self._edge.id}, " + diff --git a/src/Model/Section/SectionList.py b/src/Model/Friction/FrictionList.py similarity index 54% rename from src/Model/Section/SectionList.py rename to src/Model/Friction/FrictionList.py index d1f88f08..a82c9f22 100644 --- a/src/Model/Section/SectionList.py +++ b/src/Model/Friction/FrictionList.py @@ -1,35 +1,47 @@ # -*- coding: utf-8 -*- +import logging + from copy import copy from tools import trace, timer from Model.DB import SQLSubModel -from Model.Section.Section import Section +from Model.Friction.Friction import Friction -class SectionList(SQLSubModel): +logger = logging.getLogger() + +class FrictionList(SQLSubModel): _sub_classes = [ - Section + Friction ] def __init__(self, status = None): - super(SectionList, self).__init__() + super(FrictionList, self).__init__() self._status = status - self._sections = [] + self._frictions = [] @classmethod def _sql_create(cls, execute): return cls._create_submodel(execute) + @classmethod + def _sql_update_0_0_1(cls, execute, version): + execute("ALTER TABLE `section` RENAME TO `friction`") + @classmethod def _sql_update(cls, execute, version): + if version == "0.0.0": + logger.info(f"Update friction TABLE from {version}") + cls._sql_update_0_0_1(execute, version) + return True @classmethod def _sql_load(cls, execute, data = None): new = cls(status = data['status']) - new._sections = Section._sql_load( + new._frictions = Friction._sql_load( execute, data ) @@ -37,67 +49,67 @@ class SectionList(SQLSubModel): def _sql_save(self, execute, data = None): reach = data["reach"] - execute(f"DELETE FROM section WHERE reach = {reach.id}") + execute(f"DELETE FROM friction WHERE reach = {reach.id}") ok = True ind = 0 - for section in self._sections: + for friction in self._frictions: data["ind"] = ind - ok &= section._sql_save(execute, data = data) + ok &= friction._sql_save(execute, data = data) ind += 1 return ok def __len__(self): - return len(self._sections) + return len(self._frictions) @property - def sections(self): - return self._sections.copy() + def frictions(self): + return self._frictions.copy() def get(self, row): - return self._sections[row] + return self._frictions[row] def set(self, row, new): - self._sections[row] = new + self._frictions[row] = new self._status.modified() def new(self, index): - n = Section(status = self._status) - self._sections.insert(index, n) + n = Friction(status = self._status) + self._frictions.insert(index, n) self._status.modified() return n def insert(self, index, new): - self._sections.insert(index, new) + self._frictions.insert(index, new) self._status.modified() - def delete(self, sections): - for section in sections: - self._sections.remove(section) + def delete(self, frictions): + for friction in frictions: + self._frictions.remove(friction) self._status.modified() def delete_i(self, indexes): - sections = list( + frictions = list( map( lambda x: x[1], filter( lambda x: x[0] in indexes, - enumerate(self._sections) + enumerate(self._frictions) ) ) ) - self.delete(sections) + self.delete(frictions) def sort(self, reverse=False, key=None): - self._sections.sort(reverse=reverse, key=key) + self._frictions.sort(reverse=reverse, key=key) self._status.modified() def move_up(self, index): - if index < len(self._sections): + if index < len(self._frictions): next = index - 1 - l = self._sections + l = self._frictions l[index], l[next] = l[next], l[index] self._status.modified() @@ -105,6 +117,6 @@ class SectionList(SQLSubModel): if index >= 0: prev = index + 1 - l = self._sections + l = self._frictions l[index], l[prev] = l[prev], l[index] self._status.modified() diff --git a/src/Model/River.py b/src/Model/River.py index cbc86f19..a19e6cc3 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -13,7 +13,7 @@ from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList from Model.LateralContribution.LateralContributionList import LateralContributionList from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict from Model.Stricklers.StricklersList import StricklersList -from Model.Section.SectionList import SectionList +from Model.Friction.FrictionList import FrictionList from Model.SolverParameters.SolverParametersList import SolverParametersList from Solver.Solvers import solver_type_list @@ -86,7 +86,7 @@ class RiverNode(Node, SQLSubModel): class RiverReach(Edge, SQLSubModel): _sub_classes = [ Reach, - SectionList, + FrictionList, ] def __init__(self, id:str, name:str, @@ -100,7 +100,7 @@ class RiverReach(Edge, SQLSubModel): ) self._reach = Reach(status=self._status, parent=self) - self._sections = SectionList(status = self._status) + self._frictions = FrictionList(status = self._status) @classmethod def _sql_create(cls, execute): @@ -121,7 +121,7 @@ class RiverReach(Edge, SQLSubModel): @classmethod def _sql_update(cls, execute, version): - return True + return cls._update_submodel(execute, version) @classmethod def _sql_load(cls, execute, data = None): @@ -149,7 +149,7 @@ class RiverReach(Edge, SQLSubModel): data["parent"] = new new._reach = Reach._sql_load(execute, data) - new._sections = SectionList._sql_load(execute, data) + new._frictions = FrictionList._sql_load(execute, data) reachs.append(new) @@ -172,7 +172,7 @@ class RiverReach(Edge, SQLSubModel): data["reach"] = self - objs = [self._reach, self._sections] + objs = [self._reach, self._frictions] return self._save_submodel(execute, objs, data) @property @@ -180,8 +180,8 @@ class RiverReach(Edge, SQLSubModel): return self._reach @property - def sections(self): - return self._sections + def frictions(self): + return self._frictions class River(Graph, SQLSubModel): _sub_classes = [ @@ -286,8 +286,8 @@ class River(Graph, SQLSubModel): return self._reach @property - def sections(self): - return self._sections + def frictions(self): + return self._frictions @property def boundary_condition(self): diff --git a/src/Model/Study.py b/src/Model/Study.py index 413838ad..bdebce47 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -24,7 +24,7 @@ class Study(SQLModel): def __init__(self, filename = None, init_new = True): # Metadata - self._version = "0.0.0" + self._version = "0.0.1" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() @@ -194,7 +194,9 @@ class Study(SQLModel): if version[0] == self._version: return True - if self._update_submodel(version): + logger.info("Update database") + + if self._update_submodel(version[0]): self.execute(f"UPDATE info SET value='{self._version}' WHERE key='version'") return True diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index eedd1215..e7d95811 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -225,17 +225,17 @@ class Mage(AbstractSolver): id = 1 for edge in edges: - sections = edge.sections + frictions = edge.frictions - for section in sections.sections: + for friction in frictions.frictions: num = f"{id:>3}" - bkp = f"{section.begin_kp:>10.3f}" - ekp = f"{section.end_kp:>10.3f}" + bkp = f"{friction.begin_kp:>10.3f}" + ekp = f"{friction.end_kp:>10.3f}" - # if section.begin_kp != section.end_kp: + # if friction.begin_kp != friction.end_kp: # print("TODO") - strickler = section.begin_strickler + strickler = friction.begin_strickler coef_1 = f"{strickler.minor:>10.3f}" coef_2 = f"{strickler.medium:>10.3f}" diff --git a/src/View/Sections/PlotStricklers.py b/src/View/Frictions/PlotStricklers.py similarity index 88% rename from src/View/Sections/PlotStricklers.py rename to src/View/Frictions/PlotStricklers.py index cdd77303..8f706782 100644 --- a/src/View/Sections/PlotStricklers.py +++ b/src/View/Frictions/PlotStricklers.py @@ -17,8 +17,8 @@ class PlotStricklers(APlot): toolbar=toolbar ) - def draw_sections(self, sections, color="r"): - lst = sections + def draw_frictions(self, frictions, color="r"): + lst = frictions lst.sort(key = lambda s: s.begin_kp) coef = flatten( @@ -71,10 +71,10 @@ class PlotStricklers(APlot): left = min(kp), right = max(kp) ) - sections = self.data.sections - if len(sections) != 0: - lst = sections.sections - self.draw_sections(lst) + frictions = self.data.frictions + if len(frictions) != 0: + lst = frictions.frictions + self.draw_frictions(lst) # HightLight kp_min, kp_max = (-1, -1) @@ -85,10 +85,10 @@ class PlotStricklers(APlot): filter( lambda s: (s.begin_kp == kp_min and s.end_kp == kp_max), - sections.sections + frictions.frictions ) ) - self.draw_sections(lst, color="b") + self.draw_frictions(lst, color="b") self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Sections/Table.py b/src/View/Frictions/Table.py similarity index 81% rename from src/View/Sections/Table.py rename to src/View/Frictions/Table.py index 47aff38e..2c96ef97 100644 --- a/src/View/Sections/Table.py +++ b/src/View/Frictions/Table.py @@ -15,14 +15,14 @@ from PyQt5.QtWidgets import ( QComboBox, ) -from View.Sections.UndoCommand import ( +from View.Frictions.UndoCommand import ( SetNameCommand, SetBeginCommand, SetEndCommand, SetBeginStricklerCommand, SetEndStricklerCommand, AddCommand, DelCommand, SortCommand, MoveCommand, PasteCommand, DuplicateCommand, ) -from View.Sections.translate import * +from View.Frictions.translate import * _translate = QCoreApplication.translate @@ -39,7 +39,7 @@ class ComboBoxDelegate(QItemDelegate): if self._mode == "stricklers": self.editor.addItems( - [_translate("Sections", "Not defined")] + + [_translate("Frictions", "Not defined")] + list( map( lambda s: str(s), @@ -79,7 +79,7 @@ class TableModel(QAbstractTableModel): self._data = data self._study = study self._undo = undo - self._sections = self._data.sections + self._frictions = self._data.frictions def flags(self, index): options = Qt.ItemIsEnabled | Qt.ItemIsSelectable @@ -88,7 +88,7 @@ class TableModel(QAbstractTableModel): return options def rowCount(self, parent): - return len(self._sections) + return len(self._frictions) def columnCount(self, parent): return len(self._headers) @@ -101,27 +101,27 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._sections.get(row).name + return self._frictions.get(row).name elif self._headers[column] == "begin_kp": - return self._sections.get(row).begin_kp + return self._frictions.get(row).begin_kp elif self._headers[column] == "end_kp": - return self._sections.get(row).end_kp + return self._frictions.get(row).end_kp elif self._headers[column] == "begin_strickler": - value = self._sections.get(row).begin_strickler + value = self._frictions.get(row).begin_strickler if value == None: - return _translate("Sections", "Not defined") + return _translate("Frictions", "Not defined") return str(value) elif self._headers[column] == "end_strickler": - value = self._sections.get(row).end_strickler + value = self._frictions.get(row).end_strickler if value == None: - return _translate("Sections", "Not defined") + return _translate("Frictions", "Not defined") return str(value) return QVariant() - def headerData(self, section, orientation, role): + def headerData(self, friction, orientation, role): if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[section]] + return table_headers[self._headers[friction]] return QVariant() @@ -135,31 +135,31 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._sections, row, value + self._frictions, row, value ) ) elif self._headers[column] == "begin_kp": self._undo.push( SetBeginCommand( - self._sections, row, value + self._frictions, row, value ) ) elif self._headers[column] == "end_kp": self._undo.push( SetEndCommand( - self._sections, row, value + self._frictions, row, value ) ) elif self._headers[column] == "begin_strickler": self._undo.push( SetBeginStricklerCommand( - self._sections, row, self._study.river.strickler(value) + self._frictions, row, self._study.river.strickler(value) ) ) elif self._headers[column] == "end_strickler": self._undo.push( SetEndStricklerCommand( - self._sections, row, self._study.river.strickler(value) + self._frictions, row, self._study.river.strickler(value) ) ) @@ -171,7 +171,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sections, row, self._data + self._frictions, row, self._data ) ) @@ -183,7 +183,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._sections, rows + self._frictions, rows ) ) @@ -195,7 +195,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SortCommand( - self._sections, False + self._frictions, False ) ) @@ -212,7 +212,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._sections, "up", row + self._frictions, "up", row ) ) @@ -220,7 +220,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._sections): + if row > len(self._frictions): return target = row @@ -229,7 +229,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._sections, "down", row + self._frictions, "down", row ) ) diff --git a/src/View/Frictions/UndoCommand.py b/src/View/Frictions/UndoCommand.py new file mode 100644 index 00000000..0d685ace --- /dev/null +++ b/src/View/Frictions/UndoCommand.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +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) + + self._frictions = frictions + self._index = index + self._old = self._frictions.get(self._index).name + self._new = new_value + + def undo(self): + self._frictions.get(self._index).name = self._old + + def redo(self): + self._frictions.get(self._index).name = self._new + +class SetBeginCommand(QUndoCommand): + def __init__(self, frictions, index, new_value): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._index = index + self._old = self._frictions.get(self._index).begin_kp + self._new = new_value + + def undo(self): + self._frictions.get(self._index).begin_kp = float(self._old) + + 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) + + self._frictions = frictions + self._index = index + self._old = self._frictions.get(self._index).end_kp + self._new = new_value + + def undo(self): + self._frictions.get(self._index).end_kp = float(self._old) + + def redo(self): + self._frictions.get(self._index).end_kp = float(self._new) + + +class SetBeginStricklerCommand(QUndoCommand): + def __init__(self, frictions, index, new_value): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._index = index + self._old = self._frictions.get(self._index).begin_strickler + self._new = new_value + + def undo(self): + self._frictions.get(self._index).begin_strickler = self._old + + 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) + + self._frictions = frictions + self._index = index + self._old = self._frictions.get(self._index).end_strickler + self._new = new_value + + def undo(self): + self._frictions.get(self._index).end_strickler = self._old + + def redo(self): + self._frictions.get(self._index).end_strickler = self._new + + +class SetEdgeCommand(QUndoCommand): + def __init__(self, frictions, index, edge): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._index = index + self._old = self._frictions.get(self._index).edge + self._new = edge + + def undo(self): + self._frictions.get(self._index).edge = self._old + + def redo(self): + self._frictions.get(self._index).edge = self._new + +class AddCommand(QUndoCommand): + def __init__(self, frictions, index, reach): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._index = index + self._reach = reach + self._new = None + + def undo(self): + self._frictions.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._frictions.new(self._index) + self._new.edge = self._reach + else: + self._frictions.insert(self._index, self._new) + +class DelCommand(QUndoCommand): + def __init__(self, frictions, rows): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._rows = rows + + self._friction = [] + for row in rows: + self._friction.append((row, self._frictions.get(row))) + self._friction.sort() + + def undo(self): + for row, el in self._friction: + self._frictions.insert(row, el) + + def redo(self): + self._frictions.delete_i(self._rows) + +class SortCommand(QUndoCommand): + def __init__(self, frictions, _reverse): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._reverse = _reverse + + self._old = self._frictions.frictions + self._indexes = None + + def undo(self): + ll = self._frictions.frictions + self._frictions.sort( + key=lambda x: self._indexes[ll.index(x)] + ) + + def redo(self): + self._frictions.sort( + reverse=self._reverse, + key=lambda x: x.name + ) + if self._indexes is None: + self._indexes = list( + map( + lambda p: self._old.index(p), + self._frictions.frictions + ) + ) + self._old = None + + +class MoveCommand(QUndoCommand): + def __init__(self, frictions, up, i): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._frictions.move_up(self._i) + else: + self._frictions.move_down(self._i) + + def redo(self): + if self._up: + self._frictions.move_up(self._i) + else: + self._frictions.move_down(self._i) + + +class PasteCommand(QUndoCommand): + def __init__(self, frictions, row, friction): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._row = row + self._friction = deepcopy(friction) + self._friction.reverse() + + def undo(self): + self._frictions.delete(self._friction) + + def redo(self): + for friction in self._friction: + self._frictions.insert(self._row, friction) + + +class DuplicateCommand(QUndoCommand): + def __init__(self, frictions, rows, friction): + QUndoCommand.__init__(self) + + self._frictions = frictions + self._rows = rows + self._friction = deepcopy(friction) + self._friction.reverse() + + def undo(self): + self._frictions.delete(self._friction) + + def redo(self): + for friction in self._frictions: + self._frictions.insert(self._rows[0], friction) diff --git a/src/View/Sections/Window.py b/src/View/Frictions/Window.py similarity index 90% rename from src/View/Sections/Window.py rename to src/View/Frictions/Window.py index 63eb4b7e..71a3e8e1 100644 --- a/src/View/Sections/Window.py +++ b/src/View/Frictions/Window.py @@ -24,18 +24,18 @@ from PyQt5.QtWidgets import ( QComboBox, QVBoxLayout, QHeaderView, QTabWidget, ) -from View.Sections.UndoCommand import ( +from View.Frictions.UndoCommand import ( PasteCommand, DuplicateCommand, ) -from View.Sections.Table import ( +from View.Frictions.Table import ( TableModel, ComboBoxDelegate ) from View.Plot.MplCanvas import MplCanvas from View.Geometry.PlotKPC import PlotKPC -from View.Sections.PlotStricklers import PlotStricklers -from View.Sections.translate import * +from View.Frictions.PlotStricklers import PlotStricklers +from View.Frictions.translate import * from View.Stricklers.Window import StricklersWindow @@ -43,16 +43,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class SectionsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Sections", study=None, parent=None): +class FrictionsWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="Frictions", study=None, parent=None): self._study = study - self._reach = self._study.river._current_reach - self._sections = self._reach.sections + self._reach = self._study.river.current_reach() + self._frictions = self._reach.frictions self.setup_title(title) - super(SectionsWindow, self).__init__( - name=self._title, ui="Sections", parent=parent + super(FrictionsWindow, self).__init__( + name=self._title, ui="Frictions", parent=parent ) self.setup_sc() @@ -174,7 +174,7 @@ class SectionsWindow(ASubMainWindow, ListedSubWindow): if len(rows) > 0: data = self._reach reach = self._reach.reach - sec = self._sections.get(rows[0]) + sec = self._frictions.get(rows[0]) highlight = (sec.begin_kp, sec.end_kp) self.plot = PlotKPC( @@ -194,7 +194,7 @@ class SectionsWindow(ASubMainWindow, ListedSubWindow): def add(self): rows = self.index_selected_rows() - if len(self._sections) == 0 or len(rows) == 0: + if len(self._frictions) == 0 or len(rows) == 0: self._table.add(0) else: self._table.add(rows[0]) diff --git a/src/View/Frictions/translate.py b/src/View/Frictions/translate.py new file mode 100644 index 00000000..c404f59c --- /dev/null +++ b/src/View/Frictions/translate.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_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"), +} diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 024f611f..10a35a6e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -32,7 +32,7 @@ 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.Sections.Window import SectionsWindow +from View.Frictions.Window import FrictionsWindow from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow @@ -62,7 +62,7 @@ define_model_action = [ "action_toolBar_network", "action_toolBar_geometry", "action_toolBar_mesh", "action_toolBar_run_meshing_tool", "action_toolBar_boundary_cond", "action_toolBar_lateral_contrib", - "action_toolBar_spills", "action_toolBar_sections", + "action_toolBar_spills", "action_toolBar_frictions", "action_toolBar_stricklers", "action_toolBar_building", "action_toolBar_initial_cond", # Menu @@ -144,7 +144,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_geometry": self.open_geometry, "action_menu_boundary_conditions": self.open_boundary_cond, "action_menu_initial_conditions": self.open_initial_conditions, - "action_menu_edit_friction": self.open_sections, + "action_menu_edit_friction": self.open_frictions, "action_menu_edit_lateral_contribution": self.open_lateral_contrib, "action_menu_run_solver": self.run_solver, ## Help @@ -164,12 +164,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_toolBar_lateral_contrib": self.open_lateral_contrib, "action_toolBar_spills": lambda: self.open_dummy("Deversement"), "action_toolBar_stricklers": self.open_stricklers, - "action_toolBar_sections": self.open_sections, + "action_toolBar_frictions": self.open_frictions, "action_toolBar_building": lambda: self.open_dummy("Ouvrages"), "action_toolBar_initial_cond": self.open_initial_conditions, } for action in actions: + logger.debug("Setup connection : " + action) self.findChild(QAction, action)\ .triggered.connect(actions[action]) # action.triggered.connect(actions[action]) @@ -461,14 +462,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) strick.show() - def open_sections(self): + def open_frictions(self): if (self.model is not None and self.model.river.has_current_reach()): - sections = SectionsWindow( + frictions = FrictionsWindow( study = self.model, parent = self ) - sections.show() + frictions.show() else: self.msg_select_reach() diff --git a/src/View/Sections/UndoCommand.py b/src/View/Sections/UndoCommand.py deleted file mode 100644 index 9ba5cff6..00000000 --- a/src/View/Sections/UndoCommand.py +++ /dev/null @@ -1,226 +0,0 @@ -# -*- coding: utf-8 -*- - -from copy import deepcopy -from tools import trace, timer - -from PyQt5.QtWidgets import ( - QMessageBox, QUndoCommand, QUndoStack, -) - -from Model.Section.Section import Section -from Model.Section.SectionList import SectionList - -class SetNameCommand(QUndoCommand): - def __init__(self, sections, index, new_value): - QUndoCommand.__init__(self) - - self._sections = sections - self._index = index - self._old = self._sections.get(self._index).name - self._new = new_value - - def undo(self): - self._sections.get(self._index).name = self._old - - def redo(self): - self._sections.get(self._index).name = self._new - -class SetBeginCommand(QUndoCommand): - def __init__(self, sections, index, new_value): - QUndoCommand.__init__(self) - - self._sections = sections - self._index = index - self._old = self._sections.get(self._index).begin_kp - self._new = new_value - - def undo(self): - self._sections.get(self._index).begin_kp = float(self._old) - - def redo(self): - self._sections.get(self._index).begin_kp = float(self._new) - -class SetEndCommand(QUndoCommand): - def __init__(self, sections, index, new_value): - QUndoCommand.__init__(self) - - self._sections = sections - self._index = index - self._old = self._sections.get(self._index).end_kp - self._new = new_value - - def undo(self): - self._sections.get(self._index).end_kp = float(self._old) - - def redo(self): - self._sections.get(self._index).end_kp = float(self._new) - - -class SetBeginStricklerCommand(QUndoCommand): - def __init__(self, sections, index, new_value): - QUndoCommand.__init__(self) - - self._sections = sections - self._index = index - self._old = self._sections.get(self._index).begin_strickler - self._new = new_value - - def undo(self): - self._sections.get(self._index).begin_strickler = self._old - - def redo(self): - self._sections.get(self._index).begin_strickler = self._new - -class SetEndStricklerCommand(QUndoCommand): - def __init__(self, sections, index, new_value): - QUndoCommand.__init__(self) - - self._sections = sections - self._index = index - self._old = self._sections.get(self._index).end_strickler - self._new = new_value - - def undo(self): - self._sections.get(self._index).end_strickler = self._old - - def redo(self): - self._sections.get(self._index).end_strickler = self._new - - -class SetEdgeCommand(QUndoCommand): - def __init__(self, sections, index, edge): - QUndoCommand.__init__(self) - - self._sections = sections - self._index = index - self._old = self._sections.get(self._index).edge - self._new = edge - - def undo(self): - self._sections.get(self._index).edge = self._old - - def redo(self): - self._sections.get(self._index).edge = self._new - -class AddCommand(QUndoCommand): - def __init__(self, sections, index, reach): - QUndoCommand.__init__(self) - - self._sections = sections - self._index = index - self._reach = reach - self._new = None - - def undo(self): - self._sections.delete_i([self._index]) - - def redo(self): - if self._new is None: - self._new = self._sections.new(self._index) - self._new.edge = self._reach - else: - self._sections.insert(self._index, self._new) - -class DelCommand(QUndoCommand): - def __init__(self, sections, rows): - QUndoCommand.__init__(self) - - self._sections = sections - self._rows = rows - - self._section = [] - for row in rows: - self._section.append((row, self._sections.get(row))) - self._section.sort() - - def undo(self): - for row, el in self._section: - self._sections.insert(row, el) - - def redo(self): - self._sections.delete_i(self._rows) - -class SortCommand(QUndoCommand): - def __init__(self, sections, _reverse): - QUndoCommand.__init__(self) - - self._sections = sections - self._reverse = _reverse - - self._old = self._sections.sections - self._indexes = None - - def undo(self): - ll = self._sections.sections - self._sections.sort( - key=lambda x: self._indexes[ll.index(x)] - ) - - def redo(self): - self._sections.sort( - reverse=self._reverse, - key=lambda x: x.name - ) - if self._indexes is None: - self._indexes = list( - map( - lambda p: self._old.index(p), - self._sections.sections - ) - ) - self._old = None - - -class MoveCommand(QUndoCommand): - def __init__(self, sections, up, i): - QUndoCommand.__init__(self) - - self._sections = sections - self._up = up == "up" - self._i = i - - def undo(self): - if self._up: - self._sections.move_up(self._i) - else: - self._sections.move_down(self._i) - - def redo(self): - if self._up: - self._sections.move_up(self._i) - else: - self._sections.move_down(self._i) - - -class PasteCommand(QUndoCommand): - def __init__(self, sections, row, section): - QUndoCommand.__init__(self) - - self._sections = sections - self._row = row - self._section = deepcopy(section) - self._section.reverse() - - def undo(self): - self._sections.delete(self._section) - - def redo(self): - for section in self._section: - self._sections.insert(self._row, section) - - -class DuplicateCommand(QUndoCommand): - def __init__(self, sections, rows, section): - QUndoCommand.__init__(self) - - self._sections = sections - self._rows = rows - self._section = deepcopy(section) - self._section.reverse() - - def undo(self): - self._sections.delete(self._section) - - def redo(self): - for section in self._sections: - self._sections.insert(self._rows[0], section) diff --git a/src/View/Sections/translate.py b/src/View/Sections/translate.py deleted file mode 100644 index 6868851a..00000000 --- a/src/View/Sections/translate.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- - -from PyQt5.QtCore import QCoreApplication - -_translate = QCoreApplication.translate - -table_headers = { - "name": _translate("Sections", "Name"), - # "edge": _translate("Sections", "Reach"), - "begin_kp": _translate("Sections", "Begin kp (m)"), - "end_kp": _translate("Sections", "End kp (m)"), - "begin_strickler": _translate("Sections", "Begin strickler"), - "end_strickler": _translate("Sections", "End strickler"), -} diff --git a/src/View/ui/Sections.ui b/src/View/ui/Frictions.ui similarity index 100% rename from src/View/ui/Sections.ui rename to src/View/ui/Frictions.ui diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index eb64701c..b63f032c 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -292,7 +292,7 @@ - + @@ -858,12 +858,12 @@ Edit lateral spills - + - Sections + Friction - Edit section frictions and lateral contributions + Edit friction frictions and lateral contributions From 3ed10536199379a05ded7f5be93fe71553fdc5ec Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 17 Jul 2023 11:16:09 +0200 Subject: [PATCH 049/289] Mage: Factorize file header. --- src/Solver/Mage.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index e7d95811..125fcfff 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -7,6 +7,15 @@ from tools import timer from Solver.ASolver import AbstractSolver from Checker.Mage import MageNetworkGraphChecker +def mage_file_open(filepath, mode): + f = open(filepath, mode) + + if "w" in mode: + # Write header + f.write("* This file is generate by PAMHYR, please don't modify\n") + + return f + class Mage(AbstractSolver): _type = "mage" @@ -92,9 +101,8 @@ class Mage(AbstractSolver): if edge._name == "": name = f"Reach_{edge.id}" - with open(os.path.join(repertory, f"{name}.ST"), "w+") as f: + with mage_file_open(os.path.join(repertory, f"{name}.ST"), "w+") as f: files.append(f"{name}.ST") - f.write("* This file is generate by PAMHYR, please don't modify\n") for profile in edge.reach.profiles: num = f"{profile.num:>6}" @@ -128,11 +136,9 @@ class Mage(AbstractSolver): if qlog is not None: qlog.put(f"Export {t} file") - with open(os.path.join(repertory, f"0.{t}"), "w+") as f: + with mage_file_open(os.path.join(repertory, f"0.{t}"), "w+") as f: files.append(f"0.{t}") - f.write("* This file is generate by PAMHYR, please don't modify\n") - for bound in bounds: name = f"{bound.node.id:3}".replace(" ", "x") f.write(f"* {bound.node.name} ({name}) {bound.bctype}\n") @@ -176,11 +182,9 @@ class Mage(AbstractSolver): # if qlog is not None: # qlog.put(f"Export LAT file") - # with open(os.path.join(repertory, f"0.LAT"), "w+") as f: + # with mage_file_open(os.path.join(repertory, f"0.LAT"), "w+") as f: # files.append(f"0.LAT") - # f.write("* This file is generate by PAMHYR, please don't modify\n") - # name = f"{lateral.node.id:3}".replace(" ", "x") # f.write(f"* {lateral.node.name} ({name}) {lateral.bctype}\n") # f.write(f"${name}\n") @@ -211,9 +215,8 @@ class Mage(AbstractSolver): qlog.put("Export RUG file") # Write header - with open(os.path.join(repertory, "0.RUG"), "w+") as f: + with mage_file_open(os.path.join(repertory, "0.RUG"), "w+") as f: files.append("0.RUG") - f.write("* This file is generate by PAMHYR, please don't modify\n") edges = study.river.edges() edges = list( @@ -253,7 +256,7 @@ class Mage(AbstractSolver): qlog.put("Export INI file") # Write header - with open(os.path.join(repertory, "0.INI"), "w+") as f: + with mage_file_open(os.path.join(repertory, "0.INI"), "w+") as f: has_ini = False id = 1 reachs = study.river.edges() @@ -264,7 +267,6 @@ class Mage(AbstractSolver): ) ) - f.write("* This file is generate by PAMHYR, please don't modify\n") # TODO put real date... f.write(f"$ date en minutes : 0.00\n") f.write(f"* IB IS discharge elevation kp\n") @@ -301,9 +303,8 @@ class Mage(AbstractSolver): qlog.put("Export REP file") # Write header - with open(os.path.join(repertory, f"0.REP"), "w+") as f: + with mage_file_open(os.path.join(repertory, f"0.REP"), "w+") as f: f.write("confirmation=non\n") - f.write("* This file is generate by PAMHYR, please don't modify\n") for file in files: EXT = file.split('.')[1] @@ -372,11 +373,9 @@ class Mage8(Mage): if qlog is not None: qlog.put("Export PAR file") - with open(os.path.join(repertory, "0.PAR"), "w+") as f: + with mage_file_open(os.path.join(repertory, "0.PAR"), "w+") as f: files.append("0.PAR") - f.write("* This file is generate by PAMHYR, please don't modify\n") - params = study.river.get_params(self.type).parameters for p in params: name = p.name\ @@ -396,11 +395,9 @@ class Mage8(Mage): if qlog is not None: qlog.put("Export NET file") - with open(os.path.join(repertory, "0.NET"), "w+") as f: + with mage_file_open(os.path.join(repertory, "0.NET"), "w+") as f: files.append("0.NET") - f.write("* This file is generate by PAMHYR, please don't modify\n") - edges = study.river.edges() edges = list( filter( From 539dee31c9ba1371b9af1952927631769322225b Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 17 Jul 2023 12:04:56 +0200 Subject: [PATCH 050/289] Config: Fix config dir creation for GNU/Linux. --- src/config.py | 10 ++++++---- src/tools.py | 5 +++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/config.py b/src/config.py index c865ded2..46e4ad2e 100644 --- a/src/config.py +++ b/src/config.py @@ -242,11 +242,13 @@ class Config(SQL): file = "" if os.name == 'posix': - file = os.path.expanduser('~') + config_dir + config_file + ndir = os.path.expanduser('~') + config_dir else: - ndir = os.path.expanduser('~') + config_dir.replace("/", "\\") - os.makedirs(ndir, exist_ok = True) - file = ndir + config_file + ndir = os.path.expanduser('~') + config_dir + ndir = ndir.replace("/", "\\") + + os.makedirs(ndir, exist_ok = True) + file = ndir + config_file return file diff --git a/src/tools.py b/src/tools.py index 0e7a48d6..c71c91d1 100644 --- a/src/tools.py +++ b/src/tools.py @@ -184,6 +184,11 @@ class SQL(object): def _init_db_file(self, db): exists = Path(db).exists() + os.makedirs( + os.path.dirname(db), + exist_ok = True + ) + self._db = sqlite3.connect(db) self._cur = self._db.cursor() From 746ac01fde1acf2d985684077ebfedfc9625fc3b Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 09:36:33 +0200 Subject: [PATCH 051/289] git: Add '.gitlab-ci.yml' file. --- .gitlab-ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..7c7d3d3f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,22 @@ +stages: + - build + - test + - release + +build: + stage: build + script: + - echo "TODO build pamhyr" + +test: + stage: test + script: + - echo "TODO PAMHYR tests" + +# nightly-package: +# stage: release +# tags: +# - release +# script: +# - cd packages +# - ./linux.sh From 72b645bd784c0656f51b14713ac4571bb4764dae Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 10:22:07 +0200 Subject: [PATCH 052/289] ci: Add nightly build stage. --- .gitlab-ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7c7d3d3f..41ab1ed1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,10 +13,10 @@ test: script: - echo "TODO PAMHYR tests" -# nightly-package: -# stage: release -# tags: -# - release -# script: -# - cd packages -# - ./linux.sh +nightly-package: + stage: release + tags: + - release + script: + - cd packages + - ./linux.sh From f27e4f82b13ad45454a92623243bba59c254da69 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 10:35:39 +0200 Subject: [PATCH 053/289] ci: Minor tags change. --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 41ab1ed1..db6a1978 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,8 +15,6 @@ test: nightly-package: stage: release - tags: - - release script: - cd packages - ./linux.sh From 5fa11d42a293496767df033f602f566e24068d12 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 10:51:40 +0200 Subject: [PATCH 054/289] ci: Try release links on jobs artifacts. --- .gitlab-ci.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index db6a1978..d72cec1b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,8 +13,19 @@ test: script: - echo "TODO PAMHYR tests" -nightly-package: +nightly-linux-package: stage: release + artifacts: + paths: + - packages/pamhyr-src.tar.gz + - packages/pamhyr-gnulinux-amd64.tar.xz script: - cd packages - ./linux.sh + - | + curl --request POST \ + --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \ + --data name="pamhyr-gnulinux-amd64-nightly" \ + --data url="${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz" \ + --data direct_asset_path="/packages/pamhyr-gnulinux-amd64.tar.xz" \ + "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/nightly/assets/links" From 8dd9576d43245ef341424636520d0ff27d9da4a6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 11:26:02 +0200 Subject: [PATCH 055/289] ci: Add linux build and tab linux build. --- .gitlab-ci.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d72cec1b..f191a150 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,7 +13,7 @@ test: script: - echo "TODO PAMHYR tests" -nightly-linux-package: +linux-package: stage: release artifacts: paths: @@ -22,10 +22,21 @@ nightly-linux-package: script: - cd packages - ./linux.sh + +tag-linux-release: + stage: release + rules: + - if: $CI_COMMIT_TAG + needs: + - job: linux-package + artifacts: true + script: + - echo "POST ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${CI_COMMIT_TAG}/assets/links" + - echo " url - ${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts" - | curl --request POST \ --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \ - --data name="pamhyr-gnulinux-amd64-nightly" \ + --data name="pamhyr-gnulinux-amd64-${CI_COMMIT_TAG}" \ --data url="${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz" \ --data direct_asset_path="/packages/pamhyr-gnulinux-amd64.tar.xz" \ - "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/nightly/assets/links" + "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${CI_COMMIT_TAG}/assets/links" From 3c55e303ffa20e1f26f6827bd265ac87261f55b9 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 11:58:26 +0200 Subject: [PATCH 056/289] ci: Comment API and use release. --- .gitlab-ci.yml | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f191a150..9bb95f85 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,21 +3,41 @@ stages: - test - release +######### +# BUILD # +######### + build: stage: build script: - echo "TODO build pamhyr" +# build-lang: +# stage: build +# script: +# - cd ./src/lang/ +# - ./create_ts.sh + +######### +# TESTS # +######### + test: stage: test script: - echo "TODO PAMHYR tests" +############ +# PACKAGES # +############ + linux-package: stage: release + rules: + - if: $CI_COMMIT_BRANCH == 'master' artifacts: paths: - - packages/pamhyr-src.tar.gz + # - packages/pamhyr-src.tar.gz - packages/pamhyr-gnulinux-amd64.tar.xz script: - cd packages @@ -27,16 +47,20 @@ tag-linux-release: stage: release rules: - if: $CI_COMMIT_TAG - needs: - - job: linux-package - artifacts: true + artifacts: + paths: + - packages/pamhyr-gnulinux-amd64.tar.xz script: - - echo "POST ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${CI_COMMIT_TAG}/assets/links" - - echo " url - ${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts" - - | - curl --request POST \ - --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \ - --data name="pamhyr-gnulinux-amd64-${CI_COMMIT_TAG}" \ - --data url="${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz" \ - --data direct_asset_path="/packages/pamhyr-gnulinux-amd64.tar.xz" \ - "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${CI_COMMIT_TAG}/assets/links" + - cd packages + - ./linux.sh + release: + name: '$CI_COMMIT_TAG-release' + description: 'Automatic release from tag $CI_COMMIT_TAG' + tag_name: '$CI_COMMIT_TAG' + ref: '$CI_COMMIT_SHA' + assets: + links: + - name: 'pamhyr-gnulinux-amd64' + url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz' + filepath: '/packages/pamhyr-gnulinux-amd64.tar.xz' + link_type: 'other' From 8d0240a4bf57ff5576aa6a4e5a8fe63f9b2e753d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 14:38:36 +0200 Subject: [PATCH 057/289] ci: Add windows release. --- .gitlab-ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++--- packages/wine.sh | 10 ++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9bb95f85..4de491db 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,8 +33,13 @@ test: linux-package: stage: release + tags: + - release + - linux rules: - if: $CI_COMMIT_BRANCH == 'master' + - if: $CI_COMMIT_TAG + when: never artifacts: paths: # - packages/pamhyr-src.tar.gz @@ -43,16 +48,40 @@ linux-package: - cd packages - ./linux.sh -tag-linux-release: +windows-package: stage: release + tags: + - release + - wine + rules: + - if: $CI_COMMIT_BRANCH == 'master' + - if: $CI_COMMIT_TAG + when: never + artifacts: + paths: + - packages/pamhyr-win-amd64.zip + - packages/pamhyr-win-amd64.exe + script: + - cd packages + - ./wine.sh ci + +tag-release: + stage: release + tags: + - release + - linux + - wine rules: - if: $CI_COMMIT_TAG artifacts: paths: - packages/pamhyr-gnulinux-amd64.tar.xz + - packages/pamhyr-win-amd64.zip + - packages/pamhyr-win-amd64.exe script: - cd packages - ./linux.sh + - ./wine.sh ci release: name: '$CI_COMMIT_TAG-release' description: 'Automatic release from tag $CI_COMMIT_TAG' @@ -60,7 +89,15 @@ tag-linux-release: ref: '$CI_COMMIT_SHA' assets: links: - - name: 'pamhyr-gnulinux-amd64' + - name: 'GNU/Linux amd64 (tar.xz)' url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz' filepath: '/packages/pamhyr-gnulinux-amd64.tar.xz' - link_type: 'other' + link_type: 'Packages' + - name: 'Windows amd64 (zip)' + url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.zip' + filepath: '/packages/pamhyr-win-amd64.zip' + link_type: 'Packages' + - name: 'Windows amd64 (exe)' + url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' + filepath: '/packages/pamhyr-win-amd64.exe' + link_type: 'Packages' diff --git a/packages/wine.sh b/packages/wine.sh index 7febf68e..561716f7 100755 --- a/packages/wine.sh +++ b/packages/wine.sh @@ -4,6 +4,16 @@ echo " *** SETUP ENV" export WINARCH=win64 +if [ "$1" = "ci" ]; +then + # HACK: Set wine prefix for gitlab-ci runner + cp -rfv /home/parouby/git/pamhyr/packages/wine-pamhyr-build $PWD/wine-pamhyr-build + chown $USER -R $PWD/wine-pamhyr-build + chmod 755 $PWD/wine-pamhyr-build + export WINEPREFIX=$PWD/wine-pamhyr-build +fi + + if [ -z $WINEPREFIX ]; then # FIXME Not tested yet... From e2d06f7d1a4040c81de29421bbbe40fd81290901 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 16:04:20 +0200 Subject: [PATCH 058/289] ci: Comment wine packages (don't works). --- .gitlab-ci.yml | 56 ++++++++++++++++++++++++------------------------ packages/wine.sh | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4de491db..55cb70ab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -48,22 +48,22 @@ linux-package: - cd packages - ./linux.sh -windows-package: - stage: release - tags: - - release - - wine - rules: - - if: $CI_COMMIT_BRANCH == 'master' - - if: $CI_COMMIT_TAG - when: never - artifacts: - paths: - - packages/pamhyr-win-amd64.zip - - packages/pamhyr-win-amd64.exe - script: - - cd packages - - ./wine.sh ci +# windows-package: +# stage: release +# tags: +# - release +# - wine +# rules: +# - if: $CI_COMMIT_BRANCH == 'master' +# - if: $CI_COMMIT_TAG +# when: never +# artifacts: +# paths: +# - packages/pamhyr-win-amd64.zip +# - packages/pamhyr-win-amd64.exe +# script: +# - cd packages +# - ./wine.sh ci tag-release: stage: release @@ -76,12 +76,12 @@ tag-release: artifacts: paths: - packages/pamhyr-gnulinux-amd64.tar.xz - - packages/pamhyr-win-amd64.zip - - packages/pamhyr-win-amd64.exe + # - packages/pamhyr-win-amd64.zip + # - packages/pamhyr-win-amd64.exe script: - cd packages - ./linux.sh - - ./wine.sh ci + # - ./wine.sh ci release: name: '$CI_COMMIT_TAG-release' description: 'Automatic release from tag $CI_COMMIT_TAG' @@ -92,12 +92,12 @@ tag-release: - name: 'GNU/Linux amd64 (tar.xz)' url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz' filepath: '/packages/pamhyr-gnulinux-amd64.tar.xz' - link_type: 'Packages' - - name: 'Windows amd64 (zip)' - url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.zip' - filepath: '/packages/pamhyr-win-amd64.zip' - link_type: 'Packages' - - name: 'Windows amd64 (exe)' - url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' - filepath: '/packages/pamhyr-win-amd64.exe' - link_type: 'Packages' + link_type: 'other' + # - name: 'Windows amd64 (zip)' + # url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.zip' + # filepath: '/packages/pamhyr-win-amd64.zip' + # link_type: 'Packages' + # - name: 'Windows amd64 (exe)' + # url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' + # filepath: '/packages/pamhyr-win-amd64.exe' + # link_type: 'Packages' diff --git a/packages/wine.sh b/packages/wine.sh index 561716f7..1a769f51 100755 --- a/packages/wine.sh +++ b/packages/wine.sh @@ -6,7 +6,7 @@ export WINARCH=win64 if [ "$1" = "ci" ]; then - # HACK: Set wine prefix for gitlab-ci runner + # HACK: Set wine prefix for gitlab-ci runner (DONT WORKS) cp -rfv /home/parouby/git/pamhyr/packages/wine-pamhyr-build $PWD/wine-pamhyr-build chown $USER -R $PWD/wine-pamhyr-build chmod 755 $PWD/wine-pamhyr-build From fde96f2423d9844c43065d80f1f5768bcd4b5742 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 18 Jul 2023 16:17:22 +0200 Subject: [PATCH 059/289] ci: Minor change. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 55cb70ab..5e670bc1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -83,7 +83,7 @@ tag-release: - ./linux.sh # - ./wine.sh ci release: - name: '$CI_COMMIT_TAG-release' + name: '$CI_COMMIT_TAG' description: 'Automatic release from tag $CI_COMMIT_TAG' tag_name: '$CI_COMMIT_TAG' ref: '$CI_COMMIT_SHA' From 92712dd172f779b8dc25e11dbe82d75c4dea7c97 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 19 Jul 2023 15:11:53 +0200 Subject: [PATCH 060/289] SL: Add sediment layer Model and UI files schemes. --- src/Model/River.py | 7 + src/View/MainWindow.py | 12 +- src/View/SedimentLayers/Edit/Table.py | 0 src/View/SedimentLayers/Edit/UndoCommand.py | 0 src/View/SedimentLayers/Edit/Window.py | 0 src/View/SedimentLayers/Edit/translate.py | 0 .../SedimentLayers/Reach/Profile/Table.py | 0 .../Reach/Profile/UndoCommand.py | 0 .../SedimentLayers/Reach/Profile/Window.py | 0 .../SedimentLayers/Reach/Profile/translate.py | 0 src/View/SedimentLayers/Reach/Table.py | 0 src/View/SedimentLayers/Reach/UndoCommand.py | 0 src/View/SedimentLayers/Reach/Window.py | 0 src/View/SedimentLayers/Reach/translate.py | 0 src/View/SedimentLayers/Table.py | 151 +++++++++++++++++ src/View/SedimentLayers/UndoCommand.py | 97 +++++++++++ src/View/SedimentLayers/Window.py | 156 ++++++++++++++++++ src/View/SedimentLayers/translate.py | 10 ++ src/View/ui/EditSedimentLayers.ui | 114 +++++++++++++ src/View/ui/MainWindow.ui | 18 ++ src/View/ui/ProfileSedimentLayers.ui | 95 +++++++++++ src/View/ui/ReachSedimentLayers.ui | 98 +++++++++++ src/View/ui/SedimentLayersList.ui | 95 +++++++++++ 23 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 src/View/SedimentLayers/Edit/Table.py create mode 100644 src/View/SedimentLayers/Edit/UndoCommand.py create mode 100644 src/View/SedimentLayers/Edit/Window.py create mode 100644 src/View/SedimentLayers/Edit/translate.py create mode 100644 src/View/SedimentLayers/Reach/Profile/Table.py create mode 100644 src/View/SedimentLayers/Reach/Profile/UndoCommand.py create mode 100644 src/View/SedimentLayers/Reach/Profile/Window.py create mode 100644 src/View/SedimentLayers/Reach/Profile/translate.py create mode 100644 src/View/SedimentLayers/Reach/Table.py create mode 100644 src/View/SedimentLayers/Reach/UndoCommand.py create mode 100644 src/View/SedimentLayers/Reach/Window.py create mode 100644 src/View/SedimentLayers/Reach/translate.py create mode 100644 src/View/SedimentLayers/Table.py create mode 100644 src/View/SedimentLayers/UndoCommand.py create mode 100644 src/View/SedimentLayers/Window.py create mode 100644 src/View/SedimentLayers/translate.py create mode 100644 src/View/ui/EditSedimentLayers.ui create mode 100644 src/View/ui/ProfileSedimentLayers.ui create mode 100644 src/View/ui/ReachSedimentLayers.ui create mode 100644 src/View/ui/SedimentLayersList.ui diff --git a/src/Model/River.py b/src/Model/River.py index a19e6cc3..a46661f4 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -15,6 +15,7 @@ from Model.InitialConditions.InitialConditionsDict import InitialConditionsDict from Model.Stricklers.StricklersList import StricklersList from Model.Friction.FrictionList import FrictionList from Model.SolverParameters.SolverParametersList import SolverParametersList +from Model.SedimentLayer.SedimentLayerList import SedimentLayerList from Solver.Solvers import solver_type_list @@ -192,6 +193,7 @@ class River(Graph, SQLSubModel): InitialConditionsDict, StricklersList, SolverParametersList, + SedimentLayerList, ] def __init__(self, status=None): @@ -207,6 +209,7 @@ class River(Graph, SQLSubModel): self._initial_conditions = InitialConditionsDict(status=self._status) self._stricklers = StricklersList(status=self._status) self._parameters = {} + self._sediment_layers = SedimentLayerList() @classmethod def _sql_create(cls, execute): @@ -301,6 +304,10 @@ class River(Graph, SQLSubModel): def initial_conditions(self): return self._initial_conditions + @property + def sediment_layers(self): + return self._sediment_layers + @property def stricklers(self): return self._stricklers diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 10a35a6e..79c7ef67 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -33,6 +33,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.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow @@ -70,7 +71,8 @@ define_model_action = [ "action_menu_edit_network", "action_menu_edit_geometry", "action_menu_boundary_conditions", "action_menu_initial_conditions", "action_menu_edit_friction", "action_menu_edit_lateral_contribution", - "action_menu_run_solver", + "action_menu_run_solver", "action_menu_sediment_layers", + "action_menu_edit_reach_sediment_layers" ] action = ( @@ -147,6 +149,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_friction": self.open_frictions, "action_menu_edit_lateral_contribution": self.open_lateral_contrib, "action_menu_run_solver": self.run_solver, + "action_menu_sediment_layers": self.open_sediment_layer, ## Help "action_menu_about": self.open_about, # ToolBar action @@ -490,6 +493,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) params.show() + def open_sediment_layer(self): + sl = SedimentLayersWindow( + study = self.model, + parent = self + ) + sl.show() + def run_solver(self): if self.model is None: return diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Profile/translate.py b/src/View/SedimentLayers/Reach/Profile/translate.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/UndoCommand.py b/src/View/SedimentLayers/Reach/UndoCommand.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/SedimentLayers/Table.py b/src/View/SedimentLayers/Table.py new file mode 100644 index 00000000..7abbd869 --- /dev/null +++ b/src/View/SedimentLayers/Table.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.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) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._sl.get(row).name + elif self._headers[column] == "comment": + return self._sl.get(row).name + + 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 + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._sl, row, value + ) + ) + if self._headers[column] == "comment": + self._undo.push( + SetCommentCommand( + self._sl, row, value + ) + ) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._sl, row, self._data + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._sl, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def move_up(self, row, parent=QModelIndex()): + if row <= 0: + return + + target = row + 2 + + self.beginMoveRows(parent, row - 1, row - 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._sl, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._sl): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._sl, "down", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/UndoCommand.py b/src/View/SedimentLayers/UndoCommand.py new file mode 100644 index 00000000..84c4027b --- /dev/null +++ b/src/View/SedimentLayers/UndoCommand.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +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) + + self._sediment_layers_list = sediment_layers_list + self._index = index + self._old = self._sediment_layers_list.get(self._index).name + self._new = new_value + + def undo(self): + self._sediment_layers_list.get(self._index).name = self._old + + 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) + + self._sediment_layers_list = sediment_layers_list + self._index = index + self._old = self._sediment_layers_list.get(self._index).comment + self._new = new_value + + def undo(self): + self._sediment_layers_list.get(self._index).comment = self._old + + def redo(self): + self._sediment_layers_list.get(self._index).comment = self._new + +class AddCommand(QUndoCommand): + def __init__(self, sediment_layers_list, index, reach): + QUndoCommand.__init__(self) + + self._sediment_layers_list = sediment_layers_list + self._index = index + self._new = None + + def undo(self): + self._sediment_layers_list.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._sediment_layers_list.new(self._index) + else: + self._sediment_layers_list.insert(self._index, self._new) + +class DelCommand(QUndoCommand): + def __init__(self, sediment_layers_list, rows): + QUndoCommand.__init__(self) + + self._sediment_layers_list = sediment_layers_list + self._rows = rows + + self._sl_pos = [] + for row in rows: + self._sl_pos.append((row, self._sediment_layers_list.get(row))) + self._sl_pos.sort() + + def undo(self): + for row, el in self._sl_pos: + self._sediment_layers_list.insert(row, el) + + 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) + + self._sediment_layers_list = sediment_layers_list + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._sediment_layers_list.move_up(self._i) + else: + self._sediment_layers_list.move_down(self._i) + + def redo(self): + if self._up: + self._sediment_layers_list.move_up(self._i) + else: + self._sediment_layers_list.move_down(self._i) diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py new file mode 100644 index 00000000..22b61318 --- /dev/null +++ b/src/View/SedimentLayers/Window.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from View.ASubWindow import ASubMainWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, +) + +from View.SedimentLayers.UndoCommand import * +from View.SedimentLayers.Table import * + +from View.Plot.MplCanvas import MplCanvas +from View.SedimentLayers.translate import * + +# from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow + +_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 + + self.setup_title(title) + + super(SedimentLayersWindow, self).__init__( + name=self._title, ui="SedimentLayersList", 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): + self._table = {} + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + study = self._study, + 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) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # 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_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) + + def index_selected_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._sediment_layers) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + + def copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() + + def edit_sediment_layers(self): + rows = self.index_selected_rows() + + for row in rows: + slw = EditSedimentLayersWindow( + study = self._study, + sl = self.__sediment_layers[row], + parent = self + ) + slw.show() diff --git a/src/View/SedimentLayers/translate.py b/src/View/SedimentLayers/translate.py new file mode 100644 index 00000000..6eafe079 --- /dev/null +++ b/src/View/SedimentLayers/translate.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "name": _translate("SedimentLayers", "Name"), + "comment": _translate("SedimentLayers", "Comment"), +} diff --git a/src/View/ui/EditSedimentLayers.ui b/src/View/ui/EditSedimentLayers.ui new file mode 100644 index 00000000..7e236641 --- /dev/null +++ b/src/View/ui/EditSedimentLayers.ui @@ -0,0 +1,114 @@ + + + MainWindow + + + + 0 + 0 + 960 + 480 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 960 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add sediment layer + + + Add a new sediment layer + + + Ctrl+N + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete sediment layer + + + Delete selected sediment layer(s) + + + Ctrl+D + + + + + + ressources/up.pngressources/up.png + + + Move up + + + Move up + + + + + + ressources/down.pngressources/down.png + + + Move down + + + Move down + + + + + + diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index b63f032c..e91bd14d 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -199,10 +199,18 @@ + + + &Sediment + + + + + @@ -909,6 +917,16 @@ Initial conditions + + + Sediment layers + + + + + Edit reach sediment layers + + diff --git a/src/View/ui/ProfileSedimentLayers.ui b/src/View/ui/ProfileSedimentLayers.ui new file mode 100644 index 00000000..b9f5106d --- /dev/null +++ b/src/View/ui/ProfileSedimentLayers.ui @@ -0,0 +1,95 @@ + + + MainWindow + + + + 0 + 0 + 960 + 480 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 960 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add sediment layers + + + Add specific sediment layers on selected point(s) + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete sediment layers + + + Delete specific sediment layers of selected point(s) + + + + + + ressources/edit.pngressources/edit.png + + + Edit sediment layers + + + Edit sediment layers list + + + + + + diff --git a/src/View/ui/ReachSedimentLayers.ui b/src/View/ui/ReachSedimentLayers.ui new file mode 100644 index 00000000..b37db0c4 --- /dev/null +++ b/src/View/ui/ReachSedimentLayers.ui @@ -0,0 +1,98 @@ + + + MainWindow + + + + 0 + 0 + 960 + 480 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + Edit sediment layers list + + + + + + + Apply sediment layers on all reach + + + + + + + + + + + + + + + + 0 + 0 + 960 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + ressources/edit.pngressources/edit.png + + + Edit profile + + + Edit profile sediment layer + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add sediment layer + + + + + + diff --git a/src/View/ui/SedimentLayersList.ui b/src/View/ui/SedimentLayersList.ui new file mode 100644 index 00000000..399b0995 --- /dev/null +++ b/src/View/ui/SedimentLayersList.ui @@ -0,0 +1,95 @@ + + + MainWindow + + + + 0 + 0 + 960 + 480 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 960 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + Add sediment layer + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + Delete sediment layer + + + Delete sediment layer + + + + + + ressources/edit.pngressources/edit.png + + + Edit sediment layer + + + Edit sediment layer + + + Ctrl+E + + + + + + From 17c0035a0a7fa8b3c19bf99e3465f579e673ab9e Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 21 Jul 2023 11:01:21 +0200 Subject: [PATCH 061/289] SL: Minor change and add missing files... --- src/Model/River.py | 8 +- src/Model/SedimentLayer/SedimentLayer.py | 186 +++++++++++++++++++ src/Model/SedimentLayer/SedimentLayerList.py | 93 ++++++++++ src/View/SedimentLayers/Table.py | 4 +- src/View/SedimentLayers/UndoCommand.py | 2 +- 5 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 src/Model/SedimentLayer/SedimentLayer.py create mode 100644 src/Model/SedimentLayer/SedimentLayerList.py diff --git a/src/Model/River.py b/src/Model/River.py index a46661f4..2ff98bd8 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -209,7 +209,7 @@ class River(Graph, SQLSubModel): self._initial_conditions = InitialConditionsDict(status=self._status) self._stricklers = StricklersList(status=self._status) self._parameters = {} - self._sediment_layers = SedimentLayerList() + self._sediment_layers = SedimentLayerList(status=self._status) @classmethod def _sql_create(cls, execute): @@ -263,6 +263,12 @@ class River(Graph, SQLSubModel): data ) + # Initial conditions + new._sediment_layers = SedimentLayerList._sql_load( + execute, + data + ) + # Parameters new._parameters = SolverParametersList._sql_load( execute, diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py new file mode 100644 index 00000000..47d84df5 --- /dev/null +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from Model.DB 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, + sl = None, status = None): + super(Layer, self).__init__() + + self._status = status + self._sl = sl + + self._name = name + self._type = type + self._height = height + + if id == -1: + self.id = Layer._id_cnt + else: + self.id = id + + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE sedimentary_layer_layer( + id INTEGER NOT NULL PRIMARY KEY, + ind INTEGER NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL, + height INTEGER NOT NULL, + sl INTEGER, + FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + sl = data["sl"] + + table = execute( + "SELECT id, ind, name, type, height " + + "FROM sedimentary_layer_layer " + + f"WHERE sl = {sl.id}" + ) + + for _ in table: + new.append(None) + + for row in table: + ind = row[1] + + layer = cls( + id = row[0], name = row[2], + type = row[3], height = row[4], + sl = sl, status = data['status'] + ) + + new[ind] = layer + + return new + + def _sql_save(self, execute, data = None): + ind = data["ind"] + + sql = ( + "INSERT INTO " + + "sedimentary_layer_layer(id, ind, name, type, height, sl) "+ + "VALUES (" + + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + + f"'{self._sql_format(self._type)}', '{self._height}', {self._sl.id}" + + ")" + ) + execute(sql) + + return True + + +class SedimentLayer(SQLSubModel): + _sub_classes = [Layer] + _id_cnt = 0 + + def __init__(self, id:int = -1, + name:str = "", comment:str = "", + status = None): + super(SedimentLayer, self).__init__() + + self._status = status + self._name = name + self._comment = comment + self._layers = [] + + if id == -1: + self.id = SedimentLayer._id_cnt + else: + self.id = id + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + + @property + def comment(self): + return self._comment + + @comment.setter + def comment(self, comment): + self._comment = comment + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE sedimentary_layer( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + comment TEXT NOT NULL + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + + table = execute( + "SELECT id, name, comment" + + "FROM sedimentary_layer " + ) + + for row in table: + sl = cls( + id = row[0], + name = row[1], + comment = row[2], + status = data['status'] + ) + + data["sl"] = sl.id + sl._layers = Layer._sql_load(execute, data) + + new.append(sl) + + return new + + def _sql_save(self, execute, data = None): + if data is None: + data = {} + + sql = ( + "INSERT INTO sedimentary_layer (id, name) "+ + f"VALUES ({self.id}, '{self._sql_format(self._name)}')" + ) + execute(sql) + + ind = 0 + for l in self._layers: + data["ind"] = ind + l._sql_save(execute, data) + + ind += 1 + + return True diff --git a/src/Model/SedimentLayer/SedimentLayerList.py b/src/Model/SedimentLayer/SedimentLayerList.py new file mode 100644 index 00000000..b360c723 --- /dev/null +++ b/src/Model/SedimentLayer/SedimentLayerList.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from Model.DB import SQLSubModel +from Model.Except import NotImplementedMethodeError +from Model.SedimentLayer.SedimentLayer import SedimentLayer + +class SedimentLayerList(SQLSubModel): + _sub_classes = [SedimentLayer] + + def __init__(self, status = None): + super(SedimentLayerList, self).__init__() + + self._status = status + self._sl = [] + + def __len__(self): + return len(self._sl) + + @classmethod + def _sql_create(cls, execute): + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, execute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls(status = data["status"]) + + new._sl = SedimentLayer._sql_load(execute, data) + + return new + + def _sql_save(self, execute, data = None): + ok = True + + for sl in self._sl: + ok &= sl._sql_save(execute, data) + + return ok + + def get(self, index): + return self._sl[index] + + def set(self, index, new): + self._sl[index] = new + self._status.modified() + + def insert(self, index, new): + self._sl.insert(index, new) + self._status.modified() + + def new(self, index): + n = SedimentLayer(status = self._status) + self.insert(index, n) + self._status.modified() + return n + + def delete(self, els): + for el in els: + self._sl.remove(el) + self._status.modified() + + def delete_i(self, indexes): + els = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] in indexes, + enumerate(self._sl) + ) + ) + ) + self.delete(els) + + def move_up(self, index): + if index < len(self._sl): + next = index - 1 + + l = self._sl + l[index], l[next] = l[next], l[index] + self._status.modified() + + def move_down(self, index): + if index >= 0: + prev = index + 1 + + l = self._sl + l[index], l[prev] = l[prev], l[index] + self._status.modified() diff --git a/src/View/SedimentLayers/Table.py b/src/View/SedimentLayers/Table.py index 7abbd869..f375c318 100644 --- a/src/View/SedimentLayers/Table.py +++ b/src/View/SedimentLayers/Table.py @@ -51,7 +51,7 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": return self._sl.get(row).name elif self._headers[column] == "comment": - return self._sl.get(row).name + return self._sl.get(row).comment return QVariant() @@ -89,7 +89,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sl, row, self._data + self._sl, row ) ) diff --git a/src/View/SedimentLayers/UndoCommand.py b/src/View/SedimentLayers/UndoCommand.py index 84c4027b..f785743b 100644 --- a/src/View/SedimentLayers/UndoCommand.py +++ b/src/View/SedimentLayers/UndoCommand.py @@ -41,7 +41,7 @@ class SetCommentCommand(QUndoCommand): self._sediment_layers_list.get(self._index).comment = self._new class AddCommand(QUndoCommand): - def __init__(self, sediment_layers_list, index, reach): + def __init__(self, sediment_layers_list, index): QUndoCommand.__init__(self) self._sediment_layers_list = sediment_layers_list From 7898ab86c633262f6a279affed31bb446b2dfb2d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 21 Jul 2023 14:53:24 +0200 Subject: [PATCH 062/289] SL: Model add basic command for SL and fix DB save artifact. --- src/Model/SedimentLayer/SedimentLayer.py | 51 +++++++++++++++++++- src/Model/SedimentLayer/SedimentLayerList.py | 4 ++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 47d84df5..65684bd8 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -180,7 +180,56 @@ class SedimentLayer(SQLSubModel): for l in self._layers: data["ind"] = ind l._sql_save(execute, data) - ind += 1 return True + + def get(self, index): + return self._layers[index] + + def set(self, index, new): + self._layers[index] = new + self._status.modified() + + def insert(self, index, new): + self._layers.insert(index, new) + self._status.modified() + + def new(self, index): + n = Layer(status = self._status) + self.insert(index, n) + self._status.modified() + return n + + def delete(self, els): + for el in els: + self._layers.remove(el) + self._status.modified() + + def delete_i(self, indexes): + els = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] in indexes, + enumerate(self._layers) + ) + ) + ) + self.delete(els) + + def move_up(self, index): + if index < len(self._layers): + next = index - 1 + + l = self._layers + l[index], l[next] = l[next], l[index] + self._status.modified() + + def move_down(self, index): + if index >= 0: + prev = index + 1 + + l = self._layers + l[index], l[prev] = l[prev], l[index] + self._status.modified() diff --git a/src/Model/SedimentLayer/SedimentLayerList.py b/src/Model/SedimentLayer/SedimentLayerList.py index b360c723..32317c0b 100644 --- a/src/Model/SedimentLayer/SedimentLayerList.py +++ b/src/Model/SedimentLayer/SedimentLayerList.py @@ -37,6 +37,10 @@ class SedimentLayerList(SQLSubModel): def _sql_save(self, execute, data = None): ok = True + # Delete previous data + execute("DELETE FROM sedimentary_layer") + execute("DELETE FROM sedimentary_layer_layer") + for sl in self._sl: ok &= sl._sql_save(execute, data) From e9b00aa148e5caf1dd2b057e8cc9fd9d1fed4dbc Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 21 Jul 2023 16:11:13 +0200 Subject: [PATCH 063/289] SL: Add Edit window and fix DB save and load. --- src/Model/River.py | 1 + src/Model/SedimentLayer/SedimentLayer.py | 45 +++++- src/View/SedimentLayers/Edit/Table.py | 153 ++++++++++++++++++++ src/View/SedimentLayers/Edit/UndoCommand.py | 97 +++++++++++++ src/View/SedimentLayers/Edit/Window.py | 150 +++++++++++++++++++ src/View/SedimentLayers/Edit/translate.py | 11 ++ src/View/SedimentLayers/Window.py | 4 +- src/View/ui/EditSedimentLayers.ui | 8 +- 8 files changed, 456 insertions(+), 13 deletions(-) diff --git a/src/Model/River.py b/src/Model/River.py index 2ff98bd8..59b47957 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -282,6 +282,7 @@ class River(Graph, SQLSubModel): objs.append(self._boundary_condition) objs.append(self._initial_conditions) objs.append(self._lateral_contribution) + objs.append(self._sediment_layers) objs.append(self._stricklers) for solver in self._parameters: diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 65684bd8..7f8f1fa4 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -16,7 +16,6 @@ class Layer(SQLSubModel): super(Layer, self).__init__() self._status = status - self._sl = sl self._name = name self._type = type @@ -27,6 +26,31 @@ class Layer(SQLSubModel): else: self.id = id + Layer._id_cnt = max(id, Layer._id_cnt+1) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + + @property + def type(self): + return self._type + + @type.setter + def type(self, type): + self._type = type + + @property + def height(self): + return self._height + + @height.setter + def height(self, height): + self._height = height @classmethod def _sql_create(cls, execute): @@ -56,7 +80,7 @@ class Layer(SQLSubModel): table = execute( "SELECT id, ind, name, type, height " + "FROM sedimentary_layer_layer " + - f"WHERE sl = {sl.id}" + f"WHERE sl = {sl}" ) for _ in table: @@ -77,13 +101,14 @@ class Layer(SQLSubModel): def _sql_save(self, execute, data = None): ind = data["ind"] + sl = data["sl"] sql = ( "INSERT INTO " + "sedimentary_layer_layer(id, ind, name, type, height, sl) "+ "VALUES (" + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._type)}', '{self._height}', {self._sl.id}" + + f"'{self._sql_format(self._type)}', '{self._height}', {sl}" + ")" ) execute(sql) @@ -110,6 +135,9 @@ class SedimentLayer(SQLSubModel): else: self.id = id + def __len__(self): + return len(self._layers) + @property def name(self): return self._name @@ -147,7 +175,7 @@ class SedimentLayer(SQLSubModel): new = [] table = execute( - "SELECT id, name, comment" + + "SELECT id, name, comment " + "FROM sedimentary_layer " ) @@ -171,11 +199,14 @@ class SedimentLayer(SQLSubModel): data = {} sql = ( - "INSERT INTO sedimentary_layer (id, name) "+ - f"VALUES ({self.id}, '{self._sql_format(self._name)}')" + "INSERT INTO sedimentary_layer (id, name, comment) " + + f"VALUES ({self.id}, '{self._sql_format(self._name)}', " + + f"'{self._sql_format(self._comment)}')" ) execute(sql) + data["sl"] = self + ind = 0 for l in self._layers: data["ind"] = ind @@ -196,7 +227,7 @@ class SedimentLayer(SQLSubModel): self._status.modified() def new(self, index): - n = Layer(status = self._status) + n = Layer(sl=self, status = self._status) self.insert(index, n) self._status.modified() return n diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index e69de29b..6bd59387 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.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) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._sl.get(row).name + elif self._headers[column] == "type": + return self._sl.get(row).type + elif self._headers[column] == "height": + return self._sl.get(row).height + + 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 + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._sl, row, value + ) + ) + if self._headers[column] == "comment": + self._undo.push( + SetCommentCommand( + self._sl, row, value + ) + ) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._sl, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._sl, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def move_up(self, row, parent=QModelIndex()): + if row <= 0: + return + + target = row + 2 + + self.beginMoveRows(parent, row - 1, row - 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._sl, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._sl): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._sl, "down", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py index e69de29b..9e630a6e 100644 --- a/src/View/SedimentLayers/Edit/UndoCommand.py +++ b/src/View/SedimentLayers/Edit/UndoCommand.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +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) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).name + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).name = self._old + + def redo(self): + self._sediment_layers.get(self._index).name = self._new + +class SetHeightCommand(QUndoCommand): + def __init__(self, sediment_layers, index, new_value): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).height + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).height = self._old + + def redo(self): + self._sediment_layers.get(self._index).height = self._new + +class AddCommand(QUndoCommand): + def __init__(self, sediment_layers, index): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._index = index + self._new = None + + def undo(self): + self._sediment_layers.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._sediment_layers.new(self._index) + else: + self._sediment_layers.insert(self._index, self._new) + +class DelCommand(QUndoCommand): + def __init__(self, sediment_layers, rows): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._rows = rows + + self._sl_pos = [] + for row in rows: + self._sl_pos.append((row, self._sediment_layers.get(row))) + self._sl_pos.sort() + + def undo(self): + for row, el in self._sl_pos: + self._sediment_layers.insert(row, el) + + def redo(self): + self._sediment_layers.delete_i(self._rows) + +class MoveCommand(QUndoCommand): + def __init__(self, sediment_layers, up, i): + QUndoCommand.__init__(self) + + self._sediment_layers = sediment_layers + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._sediment_layers.move_up(self._i) + else: + self._sediment_layers.move_down(self._i) + + def redo(self): + if self._up: + self._sediment_layers.move_up(self._i) + else: + self._sediment_layers.move_down(self._i) diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index e69de29b..44100d6f 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from View.ASubWindow import ASubMainWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, +) + +from View.Plot.MplCanvas import MplCanvas + +from View.SedimentLayers.Edit.UndoCommand import * +from View.SedimentLayers.Edit.Table import * +from View.SedimentLayers.Edit.translate import * + +_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 + 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 + ) + + 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): + self._table = {} + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + study = self._study, + sl = self._sl, + 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) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # 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.delete) + self.find(QAction, "action_move_down").triggered.connect(self.delete) + + 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") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._sl) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + + def copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py index e69de29b..3b61d18c 100644 --- a/src/View/SedimentLayers/Edit/translate.py +++ b/src/View/SedimentLayers/Edit/translate.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "name": _translate("SedimentLayers", "Name"), + "type": _translate("SedimentLayers", "Type"), + "height": _translate("Sedimentlayers", "Height"), +} diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index 22b61318..b2e8f1fa 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -30,7 +30,7 @@ from View.SedimentLayers.Table import * from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.translate import * -# from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow +from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow _translate = QCoreApplication.translate @@ -150,7 +150,7 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): for row in rows: slw = EditSedimentLayersWindow( study = self._study, - sl = self.__sediment_layers[row], + sl = self._sediment_layers.get(row), parent = self ) slw.show() diff --git a/src/View/ui/EditSedimentLayers.ui b/src/View/ui/EditSedimentLayers.ui index 7e236641..6fe4c11b 100644 --- a/src/View/ui/EditSedimentLayers.ui +++ b/src/View/ui/EditSedimentLayers.ui @@ -49,12 +49,12 @@ false - - + + - + ressources/gtk-add.pngressources/gtk-add.png @@ -69,7 +69,7 @@ Ctrl+N - + ressources/gtk-remove.pngressources/gtk-remove.png From 5a29dca452d7b25e6a4c56ae3473fae9daea7e03 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 11:36:08 +0200 Subject: [PATCH 064/289] SL: Add reach SL edition window. --- src/Model/Geometry/Profile.py | 14 ++ src/Model/SedimentLayer/SedimentLayer.py | 5 +- src/Model/SedimentLayer/SedimentLayerList.py | 4 + src/View/MainWindow.py | 13 +- src/View/SedimentLayers/Reach/Table.py | 151 +++++++++++++++++++ src/View/SedimentLayers/Reach/UndoCommand.py | 32 ++++ src/View/SedimentLayers/Reach/Window.py | 150 ++++++++++++++++++ src/View/SedimentLayers/Reach/translate.py | 11 ++ src/View/SedimentLayers/Window.py | 2 - src/View/ui/ReachSedimentLayers.ui | 13 +- 10 files changed, 379 insertions(+), 16 deletions(-) diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index b0328f80..7836c47f 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -34,6 +34,7 @@ class Profile(object): self._kp = float(kp) self._name = str(name) self._reach = reach + self._sl = None self._points: List[Point] = [] @@ -120,6 +121,19 @@ class Profile(object): self._name = value.strip() self._status.modified() + @property + def sl(self): + """ + Returns: + Profile sediment layers. + """ + return self._sl + + @sl.setter + def sl(self, value: str): + self._sl = value + self._status.modified() + @property def profile_type(self): """ diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 7f8f1fa4..8099aab0 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -108,7 +108,7 @@ class Layer(SQLSubModel): "sedimentary_layer_layer(id, ind, name, type, height, sl) "+ "VALUES (" + f"{self.id}, {ind}, '{self._sql_format(self._name)}', " + - f"'{self._sql_format(self._type)}', '{self._height}', {sl}" + + f"'{self._sql_format(self._type)}', '{self._height}', {sl.id}" + ")" ) execute(sql) @@ -135,6 +135,9 @@ class SedimentLayer(SQLSubModel): else: self.id = id + def __str__(self): + return f"{self.name} ({len(self)}) - {self.comment}" + def __len__(self): return len(self._layers) diff --git a/src/Model/SedimentLayer/SedimentLayerList.py b/src/Model/SedimentLayer/SedimentLayerList.py index 32317c0b..d2b66226 100644 --- a/src/Model/SedimentLayer/SedimentLayerList.py +++ b/src/Model/SedimentLayer/SedimentLayerList.py @@ -46,6 +46,10 @@ class SedimentLayerList(SQLSubModel): return ok + @property + def sediment_layers(self): + return self._sl.copy() + def get(self, index): return self._sl[index] diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 79c7ef67..2c9b1b65 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -34,6 +34,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.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow @@ -149,7 +150,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_friction": self.open_frictions, "action_menu_edit_lateral_contribution": self.open_lateral_contrib, "action_menu_run_solver": self.run_solver, - "action_menu_sediment_layers": self.open_sediment_layer, + "action_menu_sediment_layers": self.open_sediment_layers, + "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, ## Help "action_menu_about": self.open_about, # ToolBar action @@ -493,13 +495,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) params.show() - def open_sediment_layer(self): + def open_sediment_layers(self): sl = SedimentLayersWindow( study = self.model, parent = self ) sl.show() + def open_reach_sediment_layers(self): + sl = ReachSedimentLayersWindow( + study = self.model, + parent = self + ) + sl.show() + def run_solver(self): if self.model is None: return diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py index e69de29b..d9cac095 100644 --- a/src/View/SedimentLayers/Reach/Table.py +++ b/src/View/SedimentLayers/Reach/Table.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.SedimentLayers.Reach.UndoCommand import * +from View.SedimentLayers.Reach.translate import * + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, study=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._study = study + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + self.editor.addItems( + [_translate("SedimentLayers", "Not defined")] + + list( + map( + lambda sl: str(sl), + self._study.river.sediment_layers.sediment_layers + ) + ) + ) + + self.editor.setCurrentText(index.data(Qt.DisplayRole)) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(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) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._reach.profile(row).name + if self._headers[column] == "kp": + return self._reach.profile(row).kp + if self._headers[column] == "sl": + value = self._reach.profile(row).sl + if value == None: + text = _translate("SedimentLayers", "Not defined") + return text + 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 + + row = index.row() + column = index.column() + + if self._headers[column] == "sl": + new = None + if value != _translate("SedimentLayers", "Not defined"): + new = next( + filter( + lambda sl: str(sl) == value, + self._study.river.sediment_layers.sediment_layers + ) + ) + + self._undo.push( + SetSLCommand( + self._reach, row, new + ) + ) + + self.dataChanged.emit(index, index) + return True + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/Reach/UndoCommand.py b/src/View/SedimentLayers/Reach/UndoCommand.py index e69de29b..82ba3764 100644 --- a/src/View/SedimentLayers/Reach/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/UndoCommand.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.Geometry.Reach import Reach +from Model.Geometry.Profile import Profile +from Model.SedimentLayer.SedimentLayer import SedimentLayer +from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + +logger = logging.getLogger() + +class SetSLCommand(QUndoCommand): + def __init__(self, reach, index, new_value): + QUndoCommand.__init__(self) + + self._reach = reach + self._index = index + self._old = self._reach.profile(self._index).sl + self._new = new_value + + def undo(self): + self._reach.profile(self._index).sl = self._old + + def redo(self): + self._reach.profile(self._index).sl = self._new diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index e69de29b..b292db5d 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from View.ASubWindow import ASubMainWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, +) + +from View.SedimentLayers.Reach.UndoCommand import * +from View.SedimentLayers.Reach.Table import * + +from View.Plot.MplCanvas import MplCanvas +from View.SedimentLayers.Reach.translate import * + +from View.SedimentLayers.Window import SedimentLayersWindow + +_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 + + self.setup_title(title) + + super(ReachSedimentLayersWindow, self).__init__( + name=self._title, ui="ReachSedimentLayers", 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): + 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( + study = self._study, + parent=self + ) + + table.setItemDelegateForColumn( + list(table_headers).index("sl"), + self._delegate_stricklers + ) + + 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) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # display_current = False + # ) + # self.plot.draw() + + + def setup_connections(self): + self.find(QAction, "action_edit").triggered.connect(self.edit_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) + + def index_selected_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() + + def edit_profile(self): + rows = self.index_selected_rows() + + for row in rows: + slw = ProfileSedimentLayersWindow( + study = self._study, + sl = self._sediment_layers.get(row), + parent = self + ) + slw.show() diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py index e69de29b..f6b17cf8 100644 --- a/src/View/SedimentLayers/Reach/translate.py +++ b/src/View/SedimentLayers/Reach/translate.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "name": _translate("SedimentLayers", "Name"), + "kp": _translate("SedimentLayers", "KP (m)"), + "sl": _translate("SedimentLayers", "Sediment layers"), +} diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index b2e8f1fa..b04c073d 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -68,8 +68,6 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.paste_sc = QShortcut(QKeySequence.Paste, self) def setup_table(self): - self._table = {} - table = self.find(QTableView, f"tableView") self._table = TableModel( study = self._study, diff --git a/src/View/ui/ReachSedimentLayers.ui b/src/View/ui/ReachSedimentLayers.ui index b37db0c4..a1beb8ca 100644 --- a/src/View/ui/ReachSedimentLayers.ui +++ b/src/View/ui/ReachSedimentLayers.ui @@ -69,9 +69,9 @@ false - + - + ressources/edit.pngressources/edit.png @@ -83,15 +83,6 @@ Edit profile sediment layer - - - - ressources/gtk-add.pngressources/gtk-add.png - - - Add sediment layer - - From 1b1bbb6d02ad2fd370d5711166ad82bb1b33cdf3 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 15:38:05 +0200 Subject: [PATCH 065/289] SL: Add profile edition. --- src/Model/Geometry/Point.py | 10 ++ src/Model/Geometry/Profile.py | 3 + .../SedimentLayers/Reach/Profile/Table.py | 155 ++++++++++++++++++ .../Reach/Profile/UndoCommand.py | 32 ++++ .../SedimentLayers/Reach/Profile/Window.py | 148 +++++++++++++++++ .../SedimentLayers/Reach/Profile/translate.py | 13 ++ src/View/SedimentLayers/Reach/Window.py | 3 +- 7 files changed, 363 insertions(+), 1 deletion(-) diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index 2108d962..041603df 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -9,6 +9,16 @@ class Point(object): self._status = status self._name = name + self._sl = None + + + @property + def sl(self): + return self._sl + + @sl.setter + def sl(self, sl): + self._sl = sl @property def name(self): diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 7836c47f..483082b0 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -48,6 +48,9 @@ class Profile(object): def points(self): return self._points.copy() + def point(self, index): + return self._points[index] + @property def reach(self): return self._reach diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py index e69de29b..2675e8ee 100644 --- a/src/View/SedimentLayers/Reach/Profile/Table.py +++ b/src/View/SedimentLayers/Reach/Profile/Table.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.SedimentLayers.Reach.Profile.UndoCommand import * +from View.SedimentLayers.Reach.Profile.translate import * + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, study=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._study = study + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + self.editor.addItems( + [_translate("SedimentLayers", "Not defined")] + + list( + map( + lambda sl: str(sl), + self._study.river.sediment_layers.sediment_layers + ) + ) + ) + + self.editor.setCurrentText(index.data(Qt.DisplayRole)) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup and editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(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) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._profile.point(row).name + elif self._headers[column] == "sl": + value = self._profile.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 + elif self._headers[column] == "y": + return self._profile.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 QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + + if self._headers[column] == "sl": + new = None + if value != _translate("SedimentLayers", "Not defined"): + new = next( + filter( + lambda sl: str(sl) == value, + self._study.river.sediment_layers.sediment_layers + ) + ) + + self._undo.push( + SetSLCommand( + self._profile, row, new + ) + ) + + self.dataChanged.emit(index, index) + return True + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py index e69de29b..fe3497ae 100644 --- a/src/View/SedimentLayers/Reach/Profile/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/Profile/UndoCommand.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.Geometry.Reach import Reach +from Model.Geometry.Profile import Profile +from Model.SedimentLayer.SedimentLayer import SedimentLayer +from Model.SedimentLayer.SedimentLayerList import SedimentLayerList + +logger = logging.getLogger() + +class SetSLCommand(QUndoCommand): + def __init__(self, profile, index, new_value): + QUndoCommand.__init__(self) + + self._profile = profile + self._index = index + self._old = self._profile.point(self._index).sl + self._new = new_value + + def undo(self): + self._profile.point(self._index).sl = self._old + + def redo(self): + self._profile.point(self._index).sl = self._new diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index e69de29b..8f15a27e 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from View.ASubWindow import ASubMainWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, +) + +from View.SedimentLayers.Reach.Profile.UndoCommand import * +from View.SedimentLayers.Reach.Profile.Table import * + +from View.Plot.MplCanvas import MplCanvas +from View.SedimentLayers.Reach.Profile.translate import * + +from View.SedimentLayers.Window import SedimentLayersWindow + +_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 + + self.setup_title(title) + + super(ProfileSedimentLayersWindow, self).__init__( + name=self._title, ui="ProfileSedimentLayers", parent=parent + ) + + self.setup_sc() + self.setup_table() + self.setup_graph() + self.setup_connections() + + self.ui.setWindowTitle(self._title) + + def setup_title(self, title): + rname = self._reach.name + if rname == "": + rname = _translate("SedimentLayers", "(no name)") + + pname = self._profile.name + if pname == "": + pname = _translate("SedimentLayers", "(no name)") + + self._title = ( + title + " - " + + self._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): + 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( + study = self._study, + parent=self + ) + + table.setItemDelegateForColumn( + list(table_headers).index("sl"), + self._delegate_stricklers + ) + + 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) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # display_current = False + # ) + # 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) + + def index_selected_rows(self): + table = self.find(QTableView, f"tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() diff --git a/src/View/SedimentLayers/Reach/Profile/translate.py b/src/View/SedimentLayers/Reach/Profile/translate.py index e69de29b..33e204b3 100644 --- a/src/View/SedimentLayers/Reach/Profile/translate.py +++ b/src/View/SedimentLayers/Reach/Profile/translate.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers = { + "x": _translate("SedimentLayers", "X (m)"), + "y": _translate("SedimentLayers", "Y (m)"), + "z": _translate("SedimentLazers", "Z (m)"), + "name": _translate("SedimentLayers", "Name"), + "sl": _translate("SedimentLayers", "Sediment layers"), +} diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index b292db5d..711a04fc 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -31,6 +31,7 @@ from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.Reach.translate import * from View.SedimentLayers.Window import SedimentLayersWindow +from View.SedimentLayers.Reach.Profile.Window import ProfileSedimentLayersWindow _translate = QCoreApplication.translate @@ -144,7 +145,7 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): for row in rows: slw = ProfileSedimentLayersWindow( study = self._study, - sl = self._sediment_layers.get(row), + profile = self._reach.profile(row), parent = self ) slw.show() From 3397754abc4aaf2c0423ad8379ec2238730393dd Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 16:01:53 +0200 Subject: [PATCH 066/289] SL: Fix SL edition command. --- src/View/SedimentLayers/Edit/Table.py | 10 ++++++++-- src/View/SedimentLayers/Edit/UndoCommand.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index 6bd59387..32790f1b 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -76,9 +76,15 @@ class TableModel(QAbstractTableModel): self._sl, row, value ) ) - if self._headers[column] == "comment": + if self._headers[column] == "type": self._undo.push( - SetCommentCommand( + SetTypeCommand( + self._sl, row, value + ) + ) + if self._headers[column] == "height": + self._undo.push( + SetHeightCommand( self._sl, row, value ) ) diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py index 9e630a6e..e6964709 100644 --- a/src/View/SedimentLayers/Edit/UndoCommand.py +++ b/src/View/SedimentLayers/Edit/UndoCommand.py @@ -25,6 +25,21 @@ 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) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).type + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).type = self._old + + 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) From d3edf9e5c314d9d7ab7ab08bdff0924417d7c86c Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 17:16:37 +0200 Subject: [PATCH 067/289] Geometry: Propagate profile information into point. --- src/Model/Geometry/Point.py | 3 ++- src/Model/Geometry/PointXYZ.py | 11 ++++++----- src/Model/Geometry/ProfileXYZ.py | 14 +++++++------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index 041603df..cdebd17e 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -3,12 +3,13 @@ from Model.Except import NotImplementedMethodeError class Point(object): - def __init__(self, name:str = "", status = None): + def __init__(self, name:str = "", profile=None, status = None): super(Point, self).__init__() self._status = status self._name = name + self._profile = profile self._sl = None diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 0fecd1ea..4e55c7b8 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -10,8 +10,8 @@ class PointXYZ(Point, SQLSubModel): _sub_classes = [] def __init__(self, x:float = 0.0, y:float = 0.0, z:float = 0.0, - name:str = "", status = None): - super(PointXYZ, self).__init__(name=name, status=status) + name:str = "", profile = None, status = None): + super(PointXYZ, self).__init__(name=name, profile=profile, status=status) self._x = float(x) self._y = float(y) @@ -49,7 +49,7 @@ class PointXYZ(Point, SQLSubModel): table = execute( "SELECT ind, name, x, y, z " + "FROM geometry_pointXYZ " + - f"WHERE profile = {profile}" + f"WHERE profile = {profile.id}" ) # Create points list @@ -67,6 +67,7 @@ class PointXYZ(Point, SQLSubModel): new = cls( name = name, x = x, y = y, z = z, + profile = profile, status = status ) @@ -84,7 +85,7 @@ class PointXYZ(Point, SQLSubModel): "VALUES (" + f"{ind}, '{self._sql_format(self._name)}', " + f"{self.x}, {self.y}, {self.z}, " + - f"{profile}" + + f"{profile.id}" + ")" ) execute(sql) @@ -101,7 +102,7 @@ class PointXYZ(Point, SQLSubModel): *data ) else: - valid_header = {'name', 'x', 'y', 'z'} + valid_header = {'name', 'x', 'y', 'z', 'profile'} d = {} for i, v in enumerate(data): h = header[i].strip().lower().split(' ')[0] diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index d368b981..af310769 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -102,7 +102,7 @@ class ProfileXYZ(Profile, SQLSubModel): status = status ) - data["profile"] = id + data["profile"] = new new._points = PointXYZ._sql_load(execute, data) profiles[ind] = new @@ -124,7 +124,7 @@ class ProfileXYZ(Profile, SQLSubModel): ) execute(sql) - data["profile"] = self.id + data["profile"] = self execute(f"DELETE FROM geometry_pointXYZ WHERE profile = {self.id}") ind = 0 @@ -162,11 +162,11 @@ class ProfileXYZ(Profile, SQLSubModel): try: if len(header) == 0: point = PointXYZ( - *data, status=self._status + *data, profile=self, status=self._status ) else: valid_header = {'name', 'x', 'y', 'z'} - d = {"status": self._status} + d = {"status": self._status, "profile": self} for i, v in enumerate(data): h = header[i].strip().lower().split(' ')[0] if h in valid_header: @@ -219,7 +219,7 @@ class ProfileXYZ(Profile, SQLSubModel): Nothing. """ for point in list_points: - pt = PointXYZ(*point, status=self._status) + pt = PointXYZ(*point, profile=self, status=self._status) self._points.append(pt) self._status.modified() @@ -243,7 +243,7 @@ class ProfileXYZ(Profile, SQLSubModel): Returns: Nothing. """ - point_xyz = PointXYZ(0., 0., 0., status=self._status) + point_xyz = PointXYZ(0., 0., 0., profile=self, status=self._status) self._points.append(point_xyz) self._status.modified() @@ -256,7 +256,7 @@ class ProfileXYZ(Profile, SQLSubModel): Returns: The new point. """ - point = PointXYZ(0., 0., 0., status=self._status) + point = PointXYZ(0., 0., 0., profile=self, status=self._status) self._points.insert(index, point) self._status.modified() return point From 550d86683da78e7abc64b6d82f1758e550afcc40 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 24 Jul 2023 17:55:51 +0200 Subject: [PATCH 068/289] SL: Propagate profile SL to point and fix SL id cnt. --- src/Model/Geometry/Point.py | 6 ++++++ src/Model/SedimentLayer/SedimentLayer.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index cdebd17e..98d1e348 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -15,10 +15,16 @@ class Point(object): @property def sl(self): + if self._sl is None: + return self._profile.sl + return self._sl @sl.setter def sl(self, sl): + if sl == self._profile.sl: + self._sl = None + self._sl = sl @property diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 8099aab0..c0c34abc 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -135,6 +135,8 @@ class SedimentLayer(SQLSubModel): else: self.id = id + SedimentLayer._id_cnt = max(id, SedimentLayer._id_cnt+1) + def __str__(self): return f"{self.name} ({len(self)}) - {self.comment}" From 5fab694c2705adf7e50dfe549b48421fa9ddd617 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 09:41:05 +0200 Subject: [PATCH 069/289] Geometry: Plot: Rename KPC to KPZ. --- src/View/Frictions/Window.py | 6 +++--- src/View/Geometry/{PlotKPC.py => PlotKPZ.py} | 8 ++++---- src/View/Geometry/Window.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/View/Geometry/{PlotKPC.py => PlotKPZ.py} (97%) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 71a3e8e1..18b39e3c 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -33,7 +33,7 @@ from View.Frictions.Table import ( ) from View.Plot.MplCanvas import MplCanvas -from View.Geometry.PlotKPC import PlotKPC +from View.Geometry.PlotKPZ import PlotKPZ from View.Frictions.PlotStricklers import PlotStricklers from View.Frictions.translate import * @@ -112,7 +112,7 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - self.plot = PlotKPC( + self.plot = PlotKPZ( canvas = self.canvas, data = self._reach.reach, toolbar = None, @@ -177,7 +177,7 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): sec = self._frictions.get(rows[0]) highlight = (sec.begin_kp, sec.end_kp) - self.plot = PlotKPC( + self.plot = PlotKPZ( canvas = self.canvas, data = reach, toolbar = None, diff --git a/src/View/Geometry/PlotKPC.py b/src/View/Geometry/PlotKPZ.py similarity index 97% rename from src/View/Geometry/PlotKPC.py rename to src/View/Geometry/PlotKPZ.py index f3e883f3..b37f988f 100644 --- a/src/View/Geometry/PlotKPC.py +++ b/src/View/Geometry/PlotKPZ.py @@ -13,10 +13,10 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotKPC(APlot): +class PlotKPZ(APlot): def __init__(self, canvas=None, data=None, toolbar=None, display_current=True): - super(PlotKPC, self).__init__( + super(PlotKPZ, self).__init__( canvas=canvas, data=data, toolbar=toolbar @@ -48,7 +48,7 @@ class PlotKPC(APlot): color='green', fontsize=12 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Cote (m)"), + _translate("MainWindow_reach", "Height (m)"), color='green', fontsize=12 ) @@ -218,7 +218,7 @@ class PlotKPC(APlot): kp, z_complete[i] ) except Exception as e: - logger.warning(f"Failed to update graphic KPC: {e}") + logger.warning(f"Failed to update graphic KPZ: {e}") self.canvas.axes.autoscale_view(True, True, True) self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 8efea1c3..835db523 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -22,7 +22,7 @@ from PyQt5.QtWidgets import ( ) from View.Geometry.PlotXY import PlotXY -from View.Geometry.PlotKPC import PlotKPC +from View.Geometry.PlotKPZ import PlotKPZ from View.Geometry.PlotAC import PlotAC from View.ASubWindow import WindowToolKit @@ -201,7 +201,7 @@ class GeometryWindow(QMainWindow, WindowToolKit): def plot_kpc(self): self.tableView.model().blockSignals(True) - self._plot_kpc = PlotKPC( + self._plot_kpc = PlotKPZ( canvas = self.ui.canvas_2, data = self._reach, toolbar = self.ui.toolbar_2 From ebd05768a40047de3af70f0674a686156fdc8659 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 11:35:32 +0200 Subject: [PATCH 070/289] SL: Add reach sediment layers plot. --- src/Model/Geometry/Reach.py | 33 ++++++++ src/Model/SedimentLayer/SedimentLayer.py | 4 +- src/View/SedimentLayers/Reach/Plot.py | 101 +++++++++++++++++++++++ src/View/SedimentLayers/Reach/Window.py | 15 ++-- 4 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 src/View/SedimentLayers/Reach/Plot.py diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index b18f2ade..728cf2db 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -260,6 +260,39 @@ class Reach(SQLSubModel): return max(self.get_kp()) + # Sediment Layers + + def get_sl(self): + """Get sediment layer height of profile + + Get sediment layer of profile (without spesific point sl) + + Returns: + List of sediment layers height + """ + res = [] + psl = [profile.sl for profile in self.profiles] + + # Compute max number of layers + sl_max = 0 + for sl in psl: + n = 0 if sl is None else len(sl) + sl_max = max(n, sl_max) + + # Create list of height for each sl and each layer + for i in range(0, sl_max): + cur = [] + # Compute new layer line for each sl + for sl in psl: + if sl is not None and i < len(sl): + cur.append(sl.get(i).height) + else: + cur.append(0) + # Add layer line to result + res.append(cur) + + return res + # Guidelines @timer diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index c0c34abc..f1b8a88a 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -50,7 +50,7 @@ class Layer(SQLSubModel): @height.setter def height(self, height): - self._height = height + self._height = float(height) @classmethod def _sql_create(cls, execute): @@ -60,7 +60,7 @@ class Layer(SQLSubModel): ind INTEGER NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL, - height INTEGER NOT NULL, + height REAL NOT NULL, sl INTEGER, FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) ) diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py new file mode 100644 index 00000000..af7a54ed --- /dev/null +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class Plot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None, + display_current=True): + super(Plot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + self._display_current = display_current + + self.line_kp_zmin = None + self.line_kp_sl = [] + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + if self.data.number_profiles == 0: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "Kp (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + kp = self.data.get_kp() + sl = self.data.get_sl() + z_min = self.data.get_z_min() + z_max = self.data.get_z_max() + + self.canvas.axes.set_xlim( + left = min(kp), right = max(kp) + ) + + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl, + [z_min] + ) + + for i, z in enumerate(z_sl): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + kp, z, + linestyle="solid" if i == 0 else "--", + lw=1.8, + color='grey' if i == 0 else None + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + if ind is None: + kp = self.data.get_kp() + z_min = self.data.get_z_min() + z_max = self.data.get_z_max() + + self.line_kp_zmin.set_data(kp, z_min) + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 711a04fc..4d155cec 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.Reach.UndoCommand import * from View.SedimentLayers.Reach.Table import * +from View.SedimentLayers.Reach.Plot import Plot from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.Reach.translate import * @@ -98,13 +99,13 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout_2") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # display_current = False - # ) - # self.plot.draw() + self.plot = Plot( + canvas = self.canvas, + data = self._reach, + toolbar = None, + display_current = False + ) + self.plot.draw() def setup_connections(self): From edc99230d7ecccb07f9afd9c142cc22939fdbd2a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 12:08:42 +0200 Subject: [PATCH 071/289] SL: Add cmd apply for reach. --- src/View/SedimentLayers/Reach/SLDialog.py | 58 +++++++++++++++++ src/View/SedimentLayers/Reach/Table.py | 8 +++ src/View/SedimentLayers/Reach/UndoCommand.py | 19 ++++++ src/View/SedimentLayers/Reach/Window.py | 25 ++++++++ src/View/ui/ReachSedimentLayers.ui | 4 +- src/View/ui/SLDialog.ui | 67 ++++++++++++++++++++ 6 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/View/SedimentLayers/Reach/SLDialog.py create mode 100644 src/View/ui/SLDialog.ui diff --git a/src/View/SedimentLayers/Reach/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py new file mode 100644 index 00000000..ed4d3248 --- /dev/null +++ b/src/View/SedimentLayers/Reach/SLDialog.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +from View.ASubWindow import ASubWindow +from View.ListedSubWindow import ListedSubWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QComboBox, QUndoStack, QShortcut, + QDoubleSpinBox, +) + +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 + + super(SLDialog, self).__init__( + name=title, ui="SLDialog", parent=parent + ) + + self.setup_combobox() + + self.value = None + + def setup_combobox(self): + self.combobox_add_items( + "comboBox", + [_translate("SedimentLayers", "Not defined")] + + list( + map( + lambda sl: str(sl), + self._study.river.sediment_layers.sediment_layers + ) + ) + ) + + @property + def sl(self): + return next( + filter( + lambda sl: str(sl) == self.value, + self._study.river.sediment_layers.sediment_layers + ) + ) + + def accept(self): + self.value = self.get_combobox_text("comboBox") + super().accept() diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py index d9cac095..8fdb8d7e 100644 --- a/src/View/SedimentLayers/Reach/Table.py +++ b/src/View/SedimentLayers/Reach/Table.py @@ -142,6 +142,14 @@ class TableModel(QAbstractTableModel): self.dataChanged.emit(index, index) return True + def apply_sl_each_profile(self, sl): + self._undo.push( + ApplySLCommand( + self._reach, sl + ) + ) + self.layoutChanged.emit() + def undo(self): self._undo.undo() self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/Reach/UndoCommand.py b/src/View/SedimentLayers/Reach/UndoCommand.py index 82ba3764..17e1f0f1 100644 --- a/src/View/SedimentLayers/Reach/UndoCommand.py +++ b/src/View/SedimentLayers/Reach/UndoCommand.py @@ -30,3 +30,22 @@ class SetSLCommand(QUndoCommand): def redo(self): self._reach.profile(self._index).sl = self._new + + +class ApplySLCommand(QUndoCommand): + def __init__(self, reach, new_value): + QUndoCommand.__init__(self) + + self._reach = reach + self._old = [] + for profile in self._reach.profiles: + self._old.append(profile.sl) + self._new = new_value + + def undo(self): + for i, profile in enumerate(self._reach.profiles): + profile.sl = self._old[i] + + def redo(self): + for profile in self._reach.profiles: + profile.sl = self._new diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 4d155cec..01f36348 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -27,6 +27,7 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.Reach.UndoCommand import * 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.SedimentLayers.Reach.translate import * @@ -111,6 +112,13 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def setup_connections(self): self.find(QAction, "action_edit").triggered.connect(self.edit_profile) + self.find(QPushButton, "pushButton_edit")\ + .clicked\ + .connect(self.edit_sl) + self.find(QPushButton, "pushButton_apply")\ + .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) @@ -140,6 +148,15 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def redo(self): self._table.redo() + def apply_sl_each_profile(self): + slw = SLDialog( + study = self._study, + parent = self + ) + if slw.exec(): + sl = slw.sl + self._table.apply_sl_each_profile(sl) + def edit_profile(self): rows = self.index_selected_rows() @@ -150,3 +167,11 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): parent = self ) slw.show() + + + def edit_sl(self): + slw = SedimentLayersWindow( + study = self._study, + parent = self + ) + slw.show() diff --git a/src/View/ui/ReachSedimentLayers.ui b/src/View/ui/ReachSedimentLayers.ui index a1beb8ca..dfe39a82 100644 --- a/src/View/ui/ReachSedimentLayers.ui +++ b/src/View/ui/ReachSedimentLayers.ui @@ -26,14 +26,14 @@ - + Edit sediment layers list - + Apply sediment layers on all reach diff --git a/src/View/ui/SLDialog.ui b/src/View/ui/SLDialog.ui new file mode 100644 index 00000000..8681418b --- /dev/null +++ b/src/View/ui/SLDialog.ui @@ -0,0 +1,67 @@ + + + Dialog + + + + 0 + 0 + 194 + 76 + + + + Dialog + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 043d8d839a9b4a79032761609dbbc0a6a1fb273a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 13:28:47 +0200 Subject: [PATCH 072/289] SL: Reach: Profile: Add Plot. --- src/Model/Geometry/Profile.py | 33 +++++++ src/View/SedimentLayers/Reach/Plot.py | 1 + src/View/SedimentLayers/Reach/Profile/Plot.py | 97 +++++++++++++++++++ .../SedimentLayers/Reach/Profile/Window.py | 15 +-- 4 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 src/View/SedimentLayers/Reach/Profile/Plot.py diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 483082b0..7199730f 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -275,6 +275,39 @@ class Profile(object): ) self._status.modified() + # Sediment Layers + + def get_sl(self): + """Get sediment layer height of points + + Get sediment layer of points (without spesific point sl) + + Returns: + List of sediment layers height + """ + res = [] + psl = [point.sl for point in self.points] + + # Compute max number of layers + sl_max = 0 + for sl in psl: + n = 0 if sl is None else len(sl) + sl_max = max(n, sl_max) + + # Create list of height for each sl and each layer + for i in range(0, sl_max): + cur = [] + # Compute new layer line for each sl + for sl in psl: + if sl is not None and i < len(sl): + cur.append(sl.get(i).height) + else: + cur.append(0) + # Add layer line to result + res.append(cur) + + return res + # Abstract method, must be implemented for in non abstract class def get_station(self): raise NotImplementedMethodeError(self, self.get_station) diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py index af7a54ed..1ab6daba 100644 --- a/src/View/SedimentLayers/Reach/Plot.py +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -58,6 +58,7 @@ class Plot(APlot): left = min(kp), right = max(kp) ) + # Compute sediment layer in function to profile z_min z_sl = reduce( lambda acc, v: acc + [ list( diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py new file mode 100644 index 00000000..b8c9b66f --- /dev/null +++ b/src/View/SedimentLayers/Reach/Profile/Plot.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class Plot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None, + display_current=True): + super(Plot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + self._display_current = display_current + + self.line_kp_zmin = None + self.line_kp_sl = [] + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + if self.data.number_points == 0: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + x = self.data.get_station() + z = self.data.z() + sl = self.data.get_sl() + + self.canvas.axes.set_xlim( + left = min(x), right = max(x) + ) + + # Compute sediment layer in function to point z + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl, + [z] + ) + + for i, zsl in enumerate(z_sl): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + x, zsl, + linestyle="solid" if i == 0 else "--", + lw=1.8, + color='grey' if i == 0 else None + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + if ind is None: + logger.info("TODO: Update") + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index 8f15a27e..a622bfb5 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import ( 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.SedimentLayers.Reach.Profile.translate import * @@ -108,13 +109,13 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # display_current = False - # ) - # self.plot.draw() + self.plot = Plot( + canvas = self.canvas, + data = self._profile, + toolbar = None, + display_current = False + ) + self.plot.draw() def setup_connections(self): From 181776f33fef562ffc57cab63019b85a6fe6f41d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 13:51:49 +0200 Subject: [PATCH 073/289] SL: Edit: Add Plot. --- src/Model/SedimentLayer/SedimentLayer.py | 14 +++ src/View/SedimentLayers/Edit/Plot.py | 104 +++++++++++++++++++++++ src/View/SedimentLayers/Edit/Window.py | 15 ++-- 3 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/View/SedimentLayers/Edit/Plot.py diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index f1b8a88a..020f247a 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -143,6 +143,15 @@ class SedimentLayer(SQLSubModel): def __len__(self): return len(self._layers) + @property + def layers(self): + return self._layers.copy() + + def height(self): + return list( + map(lambda l: l.height, self._layers) + ) + @property def name(self): return self._name @@ -151,6 +160,11 @@ class SedimentLayer(SQLSubModel): def name(self, name): self._name = name + def names(self): + return list( + map(lambda l: l.name, self._layers) + ) + @property def comment(self): return self._comment diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py new file mode 100644 index 00000000..86a1c8d9 --- /dev/null +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class Plot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None, + display_current=True): + super(Plot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + self._display_current = display_current + + self.line_kp_zmin = None + self.line_kp_sl = [] + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + x = [0,1] + z = [0,0] + sl = self.data.height() + names = ["fond"] + self.data.names() + + self.canvas.axes.set_xlim( + left = min(x), right = max(x) + ) + + lsl = [] + for i in range(len(sl)): + cur = [] + for _ in x: + cur.append(sl[i]) + lsl.append(cur) + + # Compute sediment layer in function to point z + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + lsl, + [z] + ) + + for i, zsl in enumerate(z_sl): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + x, zsl, + label=names[i], + linestyle="solid" if i == 0 else "--", + lw=1.8, + color='grey' if i == 0 else None + ) + self.canvas.axes.text(x[0] + 0.01, zsl[0] + 0.01, f'{names[i]}') + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + if ind is None: + logger.info("TODO: Update") + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index 44100d6f..a95e4ff0 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -28,6 +28,7 @@ from View.Plot.MplCanvas import MplCanvas 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 * _translate = QCoreApplication.translate @@ -91,13 +92,13 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # display_current = False - # ) - # self.plot.draw() + self.plot = Plot( + canvas = self.canvas, + data = self._sl, + toolbar = None, + display_current = False + ) + self.plot.draw() def setup_connections(self): From c5402ebb555c9db57578a9cc3bcf705a439d873a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 13:56:16 +0200 Subject: [PATCH 074/289] SL: Edit: Minor change. --- src/View/SedimentLayers/Edit/Plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index 86a1c8d9..14ebb948 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -49,7 +49,7 @@ class Plot(APlot): x = [0,1] z = [0,0] sl = self.data.height() - names = ["fond"] + self.data.names() + names = ["bottom"] + self.data.names() self.canvas.axes.set_xlim( left = min(x), right = max(x) From b87977a2d8572c11b6792543ff1b317e11ee2f10 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 14:22:50 +0200 Subject: [PATCH 075/289] SL: Save sl fk in database for point and profile. --- src/Model/Geometry/PointXYZ.py | 30 +++++++++++++++++++++++++++--- src/Model/Geometry/Profile.py | 2 +- src/Model/Geometry/ProfileXYZ.py | 30 +++++++++++++++++++++++++++--- src/Model/River.py | 14 ++++++++------ src/Model/Study.py | 2 +- 5 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 4e55c7b8..ff0a62fb 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -37,6 +37,17 @@ class PointXYZ(Point, SQLSubModel): @classmethod def _sql_update(cls, execute, version): cls._update_submodel(execute, version) + + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + execute( + """ + ALTER TABLE geometry_pointXYZ + ADD COLUMN sl INTEGER + REFERENCES sedimentary_layer(id) + """ + ) return True @classmethod @@ -47,7 +58,7 @@ class PointXYZ(Point, SQLSubModel): profile = data["profile"] table = execute( - "SELECT ind, name, x, y, z " + + "SELECT ind, name, x, y, z, sl " + "FROM geometry_pointXYZ " + f"WHERE profile = {profile.id}" ) @@ -63,6 +74,7 @@ class PointXYZ(Point, SQLSubModel): x = row[2] y = row[3] z = row[4] + sl = row[5] new = cls( name = name, @@ -71,6 +83,16 @@ class PointXYZ(Point, SQLSubModel): status = status ) + if sl == -1 or sl == None: + new._sl = None + else: + new._sl = next( + filter( + lambda s: s.id == sl, + data["sediment_layers_list"].sediment_layers + ) + ) + points[ind] = new return points @@ -79,13 +101,15 @@ class PointXYZ(Point, SQLSubModel): profile = data["profile"] ind = data["ind"] + sl = self._sl.id if self._sl is not None else -1 + sql = ( "INSERT OR REPLACE INTO " + - "geometry_pointXYZ(ind, name, x, y, z, profile) "+ + "geometry_pointXYZ(ind, name, x, y, z, profile, sl) "+ "VALUES (" + f"{ind}, '{self._sql_format(self._name)}', " + f"{self.x}, {self.y}, {self.z}, " + - f"{profile.id}" + + f"{profile.id}, {sl}" + ")" ) execute(sql) diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index 7199730f..9fa8e9ed 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -133,7 +133,7 @@ class Profile(object): return self._sl @sl.setter - def sl(self, value: str): + def sl(self, value): self._sl = value self._status.modified() diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index af310769..cb681383 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -68,6 +68,17 @@ class ProfileXYZ(Profile, SQLSubModel): @classmethod def _sql_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + execute( + """ + ALTER TABLE geometry_profileXYZ + ADD COLUMN sl INTEGER + REFERENCES sedimentary_layer(id) + """ + ) + return cls._update_submodel(execute, version) @classmethod @@ -77,7 +88,7 @@ class ProfileXYZ(Profile, SQLSubModel): reach = data["reach"] table = execute( - "SELECT id, ind, name, kp, num, code1, code2 " + + "SELECT id, ind, name, kp, num, code1, code2, sl " + "FROM geometry_profileXYZ " + f"WHERE reach = {reach}" ) @@ -93,6 +104,7 @@ class ProfileXYZ(Profile, SQLSubModel): num = row[5] code1 = row[5] code2 = row[6] + sl = row[7] new = cls( id=id, num = num, @@ -102,6 +114,16 @@ class ProfileXYZ(Profile, SQLSubModel): status = status ) + if sl == -1 or sl == None: + new._sl = None + else: + new._sl = next( + filter( + lambda s: s.id == sl, + data["sediment_layers_list"].sediment_layers + ) + ) + data["profile"] = new new._points = PointXYZ._sql_load(execute, data) @@ -113,13 +135,15 @@ class ProfileXYZ(Profile, SQLSubModel): ok = True ind = data["ind"] + sl = self._sl.id if self._sl is not None else -1 + sql = ( "INSERT OR REPLACE INTO " + - "geometry_profileXYZ(id, ind, name, reach, kp, num, code1, code2) "+ + "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}, " + - f"{self.code1}, {self.code1}" + + f"{self.code1}, {self.code1}, {sl}" + ")" ) execute(sql) diff --git a/src/Model/River.py b/src/Model/River.py index 59b47957..b21a95a9 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -233,6 +233,14 @@ class River(Graph, SQLSubModel): ) data["stricklers"] = new._stricklers + + # Initial conditions + new._sediment_layers = SedimentLayerList._sql_load( + execute, + data + ) + data["sediment_layers_list"] = new._sediment_layers + # Network new._nodes = RiverNode._sql_load( execute, @@ -263,12 +271,6 @@ class River(Graph, SQLSubModel): data ) - # Initial conditions - new._sediment_layers = SedimentLayerList._sql_load( - execute, - data - ) - # Parameters new._parameters = SolverParametersList._sql_load( execute, diff --git a/src/Model/Study.py b/src/Model/Study.py index bdebce47..fc553f94 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -24,7 +24,7 @@ class Study(SQLModel): def __init__(self, filename = None, init_new = True): # Metadata - self._version = "0.0.1" + self._version = "0.0.2" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() From 9e0fb53b604aeb0574abaccd933129d5d8b84502 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 14:48:54 +0200 Subject: [PATCH 076/289] SL: Fix database update. --- src/Model/SedimentLayer/SedimentLayer.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 020f247a..a18b88fb 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -70,6 +70,11 @@ class Layer(SQLSubModel): @classmethod def _sql_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + cls._sql_create(execute) + return True @classmethod @@ -187,6 +192,11 @@ class SedimentLayer(SQLSubModel): @classmethod def _sql_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + cls._sql_create(execute) + return True @classmethod From ef398c78b1313d9deac2c19b7676d4cbf8cc6cea Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 15:06:07 +0200 Subject: [PATCH 077/289] Debug: Add SqliteBrowser debug action. --- src/View/MainWindow.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 2c9b1b65..71301e1b 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -2,6 +2,7 @@ import os import logging +import subprocess from queue import Queue from PyQt5 import QtGui @@ -232,15 +233,22 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.debug_action.setStatusTip("Debug") 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.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) else: if self.conf.debug: menu.addAction(self.debug_action) + menu.addAction(self.debug_sqlite_action) self.set_debug_lvl(debug = True) else: menu.removeAction(self.debug_action) + menu.removeAction(self.debug_sqlite_action) self.set_debug_lvl(debug = False) ######### @@ -549,6 +557,17 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) repl.show() + def open_sqlite(self): + if self.model is None: + logger.debug("No study open for sql debuging...") + return + + file = self.model.filename + _ = subprocess.Popen( + f"sqlitebrowser {file}", + shell=True + ) + # TODO: Delete me ! ############### # DUMMY STUFF # From b0810f6eed640434416a25ddf1e8398b3b635808 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 15:43:02 +0200 Subject: [PATCH 078/289] SL: Fix move on edit window and some minor fix. --- src/Model/SedimentLayer/SedimentLayer.py | 2 ++ src/View/SedimentLayers/Edit/Table.py | 6 ++--- src/View/SedimentLayers/Edit/Window.py | 23 ++++++++++++++-- src/View/SedimentLayers/Window.py | 34 +++++++++++++++++------- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index a18b88fb..20af62fd 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -284,6 +284,7 @@ class SedimentLayer(SQLSubModel): l = self._layers l[index], l[next] = l[next], l[index] + self._status.modified() def move_down(self, index): @@ -292,4 +293,5 @@ class SedimentLayer(SQLSubModel): l = self._layers l[index], l[prev] = l[prev], l[index] + self._status.modified() diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index 32790f1b..47a87009 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -124,7 +124,7 @@ class TableModel(QAbstractTableModel): self.beginMoveRows(parent, row - 1, row - 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( self._sl, "up", row ) @@ -133,7 +133,7 @@ class TableModel(QAbstractTableModel): self.endMoveRows() self.layoutChanged.emit() - def move_down(self, index, parent=QModelIndex()): + def move_down(self, row, parent=QModelIndex()): if row > len(self._sl): return @@ -141,7 +141,7 @@ class TableModel(QAbstractTableModel): self.beginMoveRows(parent, row + 1, row + 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( self._sl, "down", row ) diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index a95e4ff0..965895d2 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -91,7 +91,9 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.canvas.setObjectName("canvas") self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) + self._set_plot() + def _set_plot(self): self.plot = Plot( canvas = self.canvas, data = self._sl, @@ -104,14 +106,17 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): 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.delete) - self.find(QAction, "action_move_down").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.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) + def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( @@ -138,6 +143,20 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self._table.delete(rows) + def move_up(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.move_up(rows[0]) + + def move_down(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.move_down(rows[0]) + def copy(self): logger.info("TODO: copy") diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index b04c073d..f8b9d2d2 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -27,6 +27,8 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.UndoCommand import * from View.SedimentLayers.Table import * +from View.SedimentLayers.Edit.Plot import Plot + from View.Plot.MplCanvas import MplCanvas from View.SedimentLayers.translate import * @@ -85,15 +87,6 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # 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) @@ -104,6 +97,29 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.copy_sc.activated.connect(self.copy) self.paste_sc.activated.connect(self.paste) + table = self.find(QTableView, f"tableView") + table.selectionModel()\ + .selectionChanged\ + .connect(self._set_current_sl) + + self._table.dataChanged\ + .connect(self._set_current_sl) + + def _set_current_sl(self): + rows = self.index_selected_rows() + + if len(rows) == 0: + return + + self.plot = Plot( + canvas = self.canvas, + data = self._sediment_layers.get(rows[0]), + toolbar = None, + display_current = False + ) + self.plot.draw() + + def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( From bccbc41fe822479f203902c337022ed69a227a65 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 25 Jul 2023 16:53:37 +0200 Subject: [PATCH 079/289] SL: Minor fixes. --- src/Model/SedimentLayer/SedimentLayer.py | 7 ++++++- src/View/SedimentLayers/Reach/SLDialog.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 20af62fd..c4e92621 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -143,7 +143,12 @@ class SedimentLayer(SQLSubModel): SedimentLayer._id_cnt = max(id, SedimentLayer._id_cnt+1) def __str__(self): - return f"{self.name} ({len(self)}) - {self.comment}" + s = f"{self.name} ({len(self)})" + + if self.comment != "": + s += f" - {self.comment}" + + return s def __len__(self): return len(self._layers) diff --git a/src/View/SedimentLayers/Reach/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py index ed4d3248..ce1e9794 100644 --- a/src/View/SedimentLayers/Reach/SLDialog.py +++ b/src/View/SedimentLayers/Reach/SLDialog.py @@ -46,6 +46,9 @@ class SLDialog(ASubWindow, ListedSubWindow): @property def sl(self): + if self.value == _translate("SedimentLayers", "Not defined"): + return None + return next( filter( lambda sl: str(sl) == self.value, From e9f490f076af946f3f985dbea406fc17d1c33223 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 08:57:12 +0200 Subject: [PATCH 080/289] ci: Add windows.bat and change gitlab-ci for build with wine runner. --- .gitlab-ci.yml | 86 +++++++++++++++++++++++++------------------- packages/windows.bat | 20 +++++++++++ 2 files changed, 70 insertions(+), 36 deletions(-) create mode 100644 packages/windows.bat diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5e670bc1..90bc7c9e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ stages: - build - test + - package - release ######### @@ -12,11 +13,16 @@ build: script: - echo "TODO build pamhyr" -# build-lang: -# stage: build -# script: -# - cd ./src/lang/ -# - ./create_ts.sh +build-lang: + stage: build + tags: + - linux + script: + - cd ./src/lang/ + - ./create_ts.sh + artifacts: + paths: + - src/lang/*.qm ######### # TESTS # @@ -32,56 +38,64 @@ test: ############ linux-package: - stage: release + stage: package tags: - release - linux - rules: - - if: $CI_COMMIT_BRANCH == 'master' - - if: $CI_COMMIT_TAG - when: never + needs: + - job: build-lang + artifacts: true + # rules: + # - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG + # when: never artifacts: paths: - # - packages/pamhyr-src.tar.gz - packages/pamhyr-gnulinux-amd64.tar.xz script: - cd packages - ./linux.sh -# windows-package: -# stage: release -# tags: -# - release -# - wine -# rules: -# - if: $CI_COMMIT_BRANCH == 'master' -# - if: $CI_COMMIT_TAG -# when: never -# artifacts: -# paths: -# - packages/pamhyr-win-amd64.zip -# - packages/pamhyr-win-amd64.exe -# script: -# - cd packages -# - ./wine.sh ci +windows-package: + stage: package + tags: + - release + - wine + needs: + - job: build-lang + artifacts: true + # rules: + # - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG + # when: never + artifacts: + paths: + - packages/pamhyr-win-amd64.exe + script: + - cd packages + - ./windows.bat + +########### +# RELEASE # +########### tag-release: stage: release tags: - release - linux - - wine + needs: + - job: linux-package + artifacts: true + - job: windows-package + artifacts: true rules: - if: $CI_COMMIT_TAG artifacts: paths: - packages/pamhyr-gnulinux-amd64.tar.xz - # - packages/pamhyr-win-amd64.zip - # - packages/pamhyr-win-amd64.exe script: - cd packages - - ./linux.sh - # - ./wine.sh ci + # - ./linux.sh + # # - ./wine.sh ci release: name: '$CI_COMMIT_TAG' description: 'Automatic release from tag $CI_COMMIT_TAG' @@ -97,7 +111,7 @@ tag-release: # url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.zip' # filepath: '/packages/pamhyr-win-amd64.zip' # link_type: 'Packages' - # - name: 'Windows amd64 (exe)' - # url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' - # filepath: '/packages/pamhyr-win-amd64.exe' - # link_type: 'Packages' + - name: 'Windows amd64 (exe)' + url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' + filepath: '/packages/pamhyr-win-amd64.exe' + link_type: 'other' diff --git a/packages/windows.bat b/packages/windows.bat new file mode 100644 index 00000000..42c46ba0 --- /dev/null +++ b/packages/windows.bat @@ -0,0 +1,20 @@ +rem Windows batch for pamhyr windows version building +@ECHO ON + +rem Python environment +python -m pip install -r ..\requirements.txt + +rem Build windows version +pyinstaller ..\src\pamhyr.py -y + +rem Copy data +mkdir dist\pamhyr\View\ui\ressources +mkdir dist\pamhyr\View\ui\Widgets + +copy /y ..\src\View\ui\ressources\ dist\pamhyr\View\ui\ressources +copy /y ..\src\View\ui\Widgets\*.ui dist\pamhyr\View\ui\Widgets +copy /y ..\src\View\ui\*.ui dist\pamhyr\View\ui\ +copy /y ..\src\lang\*.qm dist\pamhyr\lang\ + +rem Make installer +"C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi From 41961a786e6fb43728153f640ddb4ae30c8d9dbf Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 11:41:23 +0200 Subject: [PATCH 081/289] ci: Disable windows build. --- .gitlab-ci.yml | 51 ++++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 90bc7c9e..ec641795 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -55,23 +55,23 @@ linux-package: - cd packages - ./linux.sh -windows-package: - stage: package - tags: - - release - - wine - needs: - - job: build-lang - artifacts: true - # rules: - # - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG - # when: never - artifacts: - paths: - - packages/pamhyr-win-amd64.exe - script: - - cd packages - - ./windows.bat +# windows-package: +# stage: package +# tags: +# - release +# - wine +# needs: +# - job: build-lang +# artifacts: true +# # rules: +# # - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG +# # when: never +# artifacts: +# paths: +# - packages/pamhyr-win-amd64.exe +# script: +# - cd packages +# - ./windows.bat ########### # RELEASE # @@ -85,13 +85,14 @@ tag-release: needs: - job: linux-package artifacts: true - - job: windows-package - artifacts: true + # - job: windows-package + # artifacts: true rules: - if: $CI_COMMIT_TAG artifacts: paths: - packages/pamhyr-gnulinux-amd64.tar.xz + - packages/pamhyr-win-amd64.exe script: - cd packages # - ./linux.sh @@ -107,11 +108,7 @@ tag-release: url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz' filepath: '/packages/pamhyr-gnulinux-amd64.tar.xz' link_type: 'other' - # - name: 'Windows amd64 (zip)' - # url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.zip' - # filepath: '/packages/pamhyr-win-amd64.zip' - # link_type: 'Packages' - - name: 'Windows amd64 (exe)' - url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' - filepath: '/packages/pamhyr-win-amd64.exe' - link_type: 'other' + # - name: 'Windows amd64 (exe)' + # url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' + # filepath: '/packages/pamhyr-win-amd64.exe' + # link_type: 'other' From a9b905a03957e8dfd37767f4be785213d7902c98 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 12:15:16 +0200 Subject: [PATCH 082/289] SL: Reach/Profile: Add update. --- src/View/SedimentLayers/Reach/Profile/Window.py | 9 +++++++++ src/View/SedimentLayers/Reach/Window.py | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index a622bfb5..93b201a6 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -109,6 +109,9 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) + self._update_plot() + + def _update_plot(self): self.plot = Plot( canvas = self.canvas, data = self._profile, @@ -124,6 +127,12 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): 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) + + def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 01f36348..ee477edc 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -100,6 +100,9 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout_2") self.plot_layout.addWidget(self.canvas) + self._update_plot() + + def _update_plot(self): self.plot = Plot( canvas = self.canvas, data = self._reach, @@ -124,6 +127,12 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): 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) + + def index_selected_rows(self): table = self.find(QTableView, f"tableView") return list( @@ -144,9 +153,11 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def undo(self): self._table.undo() + self._update_plot() def redo(self): self._table.redo() + self._update_plot() def apply_sl_each_profile(self): slw = SLDialog( @@ -157,6 +168,8 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): sl = slw.sl self._table.apply_sl_each_profile(sl) + self._update_plot() + def edit_profile(self): rows = self.index_selected_rows() From 003be29ecac20afa5fd58956b1b997b0be3415fe Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 13:26:41 +0200 Subject: [PATCH 083/289] SL: Fix move down exception. --- src/Model/SedimentLayer/SedimentLayer.py | 4 ++-- src/View/SedimentLayers/Edit/Table.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index c4e92621..1bbcb602 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -284,7 +284,7 @@ class SedimentLayer(SQLSubModel): self.delete(els) def move_up(self, index): - if index < len(self._layers): + if index >= 0: next = index - 1 l = self._layers @@ -293,7 +293,7 @@ class SedimentLayer(SQLSubModel): self._status.modified() def move_down(self, index): - if index >= 0: + if index + 1 < len(self._layers): prev = index + 1 l = self._layers diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index 47a87009..91a13b3f 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -134,7 +134,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, row, parent=QModelIndex()): - if row > len(self._sl): + if row + 1 >= len(self._sl): return target = row From 02ef127942c9d4ebbcaad4c1bc66b0e76bbb8a7f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 15:11:44 +0200 Subject: [PATCH 084/289] SL: Reach: Profile: Minor change. --- src/View/SedimentLayers/Reach/Profile/Table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py index 2675e8ee..102bec32 100644 --- a/src/View/SedimentLayers/Reach/Profile/Table.py +++ b/src/View/SedimentLayers/Reach/Profile/Table.py @@ -34,7 +34,7 @@ class ComboBoxDelegate(QItemDelegate): self.editor = QComboBox(parent) self.editor.addItems( - [_translate("SedimentLayers", "Not defined")] + + # [_translate("SedimentLayers", "Not defined")] + list( map( lambda sl: str(sl), From 3f41a2e9d9ab46afeeb1b52f8419baa938dc1f92 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 16:37:01 +0200 Subject: [PATCH 085/289] Trans: Update partial french translate and minor change in mainwindow. --- src/View/ui/MainWindow.ui | 55 +- src/lang/fr.ts | 2083 +++++++++++++++++++++++++++---------- 2 files changed, 1538 insertions(+), 600 deletions(-) diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index b63f032c..6913d02a 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -85,17 +85,12 @@ - - - - - @@ -117,32 +112,8 @@ &Geometry - - - Comparer - - - - Profil en travers - - - - - - - - - - - - - - - - - @@ -152,8 +123,8 @@ &Execute + - @@ -166,14 +137,9 @@ - - - - - @@ -264,8 +230,6 @@ - - @@ -290,12 +254,8 @@ - - - - @@ -729,10 +689,10 @@ ressources/exit_bis.pngressources/exit_bis.png - Quit application + Quit - Quitter l'application (Ctrl+Q) + Quit the application (Ctrl+Q) Ctrl+Q @@ -747,7 +707,7 @@ Run solver - Run solver on current study + Run a solver @@ -828,7 +788,7 @@ Mesh - Afficher le maillage + Display meshed reach @@ -863,7 +823,7 @@ Friction - Edit friction frictions and lateral contributions + Edit friction frictions @@ -908,6 +868,9 @@ Initial conditions + + Define initial conditions + diff --git a/src/lang/fr.ts b/src/lang/fr.ts index d44729f7..9eabd6ef 100644 --- a/src/lang/fr.ts +++ b/src/lang/fr.ts @@ -1,1101 +1,2076 @@ - + - Dialog + BoundaryCondition - - Dialog + + Not associate + Non associer + + + + X - + + Y + + + + + Time + Temps + + + + Date + Date + + + + Z (m) + Z (m) + + + + days + jours + + + + day + jour + + + + Not defined + Non définie + + + + Ponctual contribution + Contributions ponctuelles + + + + Time over Z + + + + + Time over Discharge + + + + + Z over Discharge + + + + Name Nom - + Type Type - + + Node + Nœud + + + + Discharge (m³/s) + Débit (m³/s) + + + + Discharge (m³/s) + + + + + Checker + + + Mage network graph {mode} checker + + + + + Check if the network graph is valid + Vérifie si le graph réseau est valide + + + + Study network reach checker + + + + + Check if exists at least one reach for study + Vérifie si il exists au moins un Bief dans l'étude + + + + Study geometry checker + Vérificateur de géometrie de l'étude + + + + Check if exists geometry for each reach of study + Vérifie si la géométrie exists pour chaque bief de l'étude + + + + Dummy ok + + + + + Dummy warning + + + + + Dummy error + + + + + Dialog + + + Dialog + + + + + Name + Nom + + + + Type + Type + + + Description Description - + Solver - Solveur + Solver - + Path Chemin - + Output formater - Formateur de resultat + Formateur de sortie - + Command line Ligne de commande - + Input formater Formateur d'entrée - - Reverse - Retourner + + Cancel + Annuler - + + Run + Lancer + + + + Draft + Débit (m³/s) + + + Solvers - Solveurs + Solvers - + Meshing tool Mailleur - + Meshing tool path Chemin du mailleur - + Constants Constantes - + Segment number - Nombre de segment + Nombre se segments - + Listing maximum size - Taille maximum des listings + Taille maximal du listing - + 1000 - 1000 + - + 500000 - 500000 + - + Backup - Backup + Archive - + Auto save - Sauvegarde automatique + Sauvegarde auto - + Frequence Fréquence - + Max. archives - Nombre maximum de sauvegarde + Nombre max d'archive - + Enable Activé - + HH:mm:ss - + + Stricklers + + + + + Editor + Éditeur + + + + This value must be used for reading or editing files in speficic case. + Cette valeur peut être utiliser dans des cas spécifique pour lire ou écrire dans un fichier. + + + + Editor command + Commande de l'éditeur + + + + - The "@file" keyworkd is replace by the path of file to open. + - Le mot clef "@file" sera remplacer par le chemin du fichier à ouvrir. + + + Language Langage - + Please restart application after language modification - Please restart application after language modification + Please restart application after language modification - + MyNewStudy - MaNouvelleÉtude + Ma nouvelle étude + + + + Time system + Système de temps + + + + Time + Temps + + + + Date + Date + + + + Staring date + Date de départ + + + + dd/MM/yyyy HH:mm:ss + + + + + Creation date : + Date de création : + + + + Last modification : + Dernière modification : + + + + Discharge + Débit (m³/s) + + + + Exception + + + Generic error message + + + + + Undefined error message + + + + + Method not implemented + + + + + Method + + + + + not implemented + + + + + for class + + + + + Not implemented method + + + + + FileFormatError + + + + + Invalid file format: + + + + + File format error + + + + + Invalid file format + + + + + Invalid file + + + + + format because of + + + + + Clipboard format error + + + + + without header + + + + + with header + + + + + Invalid clipboard data format: + + + + + Clipboard format unknown + Form - + Form - + PamHyr - + Version en developpement: - - - MainWindow - - PAMHYR + + dd/MM/yyyy HH:mm:ss - + + days + jours + + + + HH:mm:ss + + + + + Frictions + + + Not defined + Non définie + + + + Name + Nom + + + + Begin kp (m) + Pk de départ (m) + + + + End kp (m) + Pk de fin (m) + + + + Begin strickler + strickler de départ + + + + End strickler + Strickler de fin + + + + Geometry + + + Name + Nom + + + + Kp (m) + Pk (m) + + + + Type + Type + + + + upstream + amont + + + + downstream + aval + + + + LateralContribution + + + Name + Nom + + + + Minor bed + Lit mineur + + + + Medium bed + Lit moyen + + + + Comment + Commentaire + + + + Not associate + Non associer + + + + X + + + + + Y + + + + + Time + Temps + + + + Date + Date + + + + Z (m) + + + + + days + jours + + + + day + jour + + + + Not defined + Non définie + + + + Lateral contribution + Contribution laterale + + + + Rain + Pluie + + + + Evaporation + Évaporation + + + + Type + Type + + + + Reach + Bief + + + + Begin kp (m) + Pk de départ (m) + + + + End kp (m) + Pk de fin (m) + + + + KP (m) + PK (m) + + + + Elevation (m) + Altitude (m) + + + + Height (m) + Hauteur (m) + + + + Discharge (m³/s) + Débit (m³/s) + + + + Discharge (m³/s) + + + + + MainWindow + + + MainWindow + Fenêtre principale + + + + toolBar + Bar d'outils + + + + Add node or edge + Ajouter un nœud ou une arête + + + + Remove node or edge + Supprimer un nœud ou une arête + + + + PAMHYR + PAMHYR + + + &File &Fichier - + &River Network &Réseau - + &Geometry &Géométrie - + Comparer - Comparer + Comparer - - Profil en travers - - - - + &Execute - &Exécuter + &Executer - + &Hydraulics - &Hydrolique + &Hydraulique - + &Plots - &Graphiques + Gra&phique - + &Cartography &Cartographie - + &Help &Aide - - toolBar - - - - + toolBar_2 - + - + New study Nouvelle étude - + Ctrl+N - + - + Open a study Ouvrir une étude - + Ctrl+O - + - + Import data from MAGE - Importer un jeu de données MAGE + Importer des données d'une étude MAGE - + Import data from RubarBE - Importer un jeu de données RubarBE + Importer des données d'une étude RubarBE - + Close Fermer - + Close current study - Fermer l'étude courante + Fermer l'étude en cours - + Save mesh Sauvegarder le maillage - + Save Sauvegarder - + Ctrl+S - + - + Save as ... - Sauvegarder sous ... + Sauvegarder sous... - + Ctrl+Shift+S - + - + Archive - + Archive - + Pamhyr configuration - Configuration de PamHyr + Configuration de PAMHYR - + Quit Quitter - + Ctrl+F4 - + - + Edit river network Éditer le réseau - + Edit geometry Éditer la géométrie - + Import geometry - Importer une géometrie + Importer une géométrie - + Export geometry - + Exporter la géométrie - + Run extrenal meshing tool - + Lancer le mailler externe - + choose meshing tool by reach - + View meshed geometry - + Voir la géométrie mailler - + Export mesh - + Exporter le maillage - + Delete mesh of current reach - + Supprimer le maillage - + Delete all mesh - + - + Abscisse - Cote - + XYZ - + - - Numerical parameter for MAGE solver - + + Numerical parameter for solvers + Paramètre numerique des solvers - + Boundary conditions and one-time contributions - + Condition aux limites et apports ponctuels - + Initial conditions - + Conditions initiales - + Export initial conditions - + Exporter les conditions initiales - + Import final state as initial condition - + Importer un état final comme conditions initiales - + Edit friction - + Éditer les frottements - + Edit lateral contributions - + Éditer les apports latéraux - + Edit spills - + Edit cross building - + Éditer les ouvrages - + Run solver - + Lancer un solver - + + F5 + + + + Stop solver - + Stopper le solver - + Display listings - + Simulation directory management - + Open - Ouvrir + Ouvrir - + Hydrograph - + Hydrogramme - + Limnigram - + Limnigramme - + Map current reach - + Cartographier le bief sélectionné - + Help PAMHYR - + Aide de PAMHYR - - help MAGE - + + Help MAGE + Aide de MAGE - + About - + A propos - + ouvrir - + Save current study - + Sauvegarder l'étude - + Ctrl+F - + - + Quit application - + Quitter l'application - - Quitter l'application (Ctrl+Q) - - - - + Ctrl+Q - + - - Run solver on current study - - - - - Ctrl+X - - - - + stop solver - + Interrompt la simulation en cours - + Ctrl+C - + - + Run external meshing tool - + Lancer le mailler externe - + Run meshing tool on current reach geometry - + Lancer le mailler externe sur le bief selectionné - + Display simulation listing - + Display current simulation listing - + River network - + Réseau - + Geometry - + Géométrie - + Edit reach geometry - + Éditer la géométrie - + Mesh - + Maillage - - Afficher le maillage - - - - + Boundary conditions - + Conditions aux limites - + Edit boundary conditions and one-time contributions - + Éditer les conditions aux limites et les apports ponctuels - + Lateral contribution - + Contributions latérales - + Edit lateral contribution - + Éditer les contributions latérales - + Spills - + Edit lateral spills - - Sections - + + Friction + Frottements - - Edit section frictions and lateral contributions - + + Stricklers + - - Frictions - + + Edit the study stricklers + Éditer les Stricklers de l'étude - - Edit friction at the bottom - - - - + Building - + Ouvrages - + Edit building (valve, ...), singularity and pump - + Éditer les ouvrages - + Edit study - + Éditer l'étude - + English - + - + French + + + + + Revert + Retourner + + + + Open in editor + Ouvrir dans l'éditeur + + + + Eval + Évaluer + + + + Ctrl+Return + + + + + Liquid + Liquide + + + + Solid + Solide + + + + Suspenssion + Suspenssion + + + + Add + Ajouter + + + + Add a new boundary condition or lateral contribution + Ajouter une condition aux limites ou un apport ponctuel + + + + Delete + Supprimer + + + + Delete current selected rows + Supprimer les lignes selectionnées + + + + Ctrl+D + + + + + Edit + Éditer + + + + Edit boundary condition or lateral contribution + Éditer une condition aux limites ou un apport ponctuel + + + + Ctrl+E + + + + + Sort + Trier + + + + Sort boundary condition by name + Trié par nom + + + + Study stricklers + Stricklers de l'étude + + + + Application stricklers + Stricklers de l'application + + + + Add new stricklers + Ajouter un stricklers + + + + Delete selected stricklers + Supprimer les stricklers selectionnés + + + + Sort stricklers + Trier les stricklers + + + + delete + Supprimer + + + + Edit stricklers + Éditer les stricklers + + + + Cancel + Annuler + + + + Retry + Ressayer + + + + Run + Lancer + + + + Retry check + Ressayer la vérification + + + + Stop + Stopper + + + + Start + Commencer + + + + Pause + Pause + + + + LogFile + Fichier de log + + + + Generate minimal height + Généré une hauteur minimale + + + + Generate constant discharge + Généré un debit constant + + + + Add new initial condition + Ajouter une nouvelle condition initiale + + + + Delete inital condition + Supprimer une condition initiale + + + + sort + Trier + + + + Sort inital condition + Trier les conditions initiales + + + + Add a new point in boundary condition or lateral contribution + Ajouter un nouveau point + + + + Sort boundary condition point + Trier les points des conditions aux limites + + + + Quit the application (Ctrl+Q) - - &Fichier + + Run a solver + Lancer un solver + + + + Display meshed reach - - &Hydraulique - + + Edit friction frictions + Éditer les frottements - - &Graphiques - - - - - &Cartographie - - - - - &Aide - - - - - Ctrl+R - - - - - Fermer - - - - - Enregistrer le maillage - - - - - Enregistrer - - - - - Enregistrer sous ... - - - - - Archiver - - - - - Configuration de Pamhyr - - - - - Quitter - - - - - Lancer le mailleur externe - - - - - Choix du mailleur par bief - - - - - Exporter le maillage - - - - - Supprimer le maillage du bief courant - - - - - Supprimer l'ensemble des maillages - - - - - Conditions aux Limites & Apports Ponctuels - - - - - Conditions initiales - - - - - Solveur MAGE - - - - - Stop Solveur - - - - - Afficher les listings - - - - - Ouvrir - - - - - Hydrogramme - - - - - Limnigramme - - - - - Ligne d'eau - - - - - Ligne d'eau finale - - - - - Ligne d'eau enveloppe - - - - - Voir l'animation (MAGE) - - - - - Cartographier le bief courant - - - - - Aide de PAMHYR - - - - - Aide de MAGE - - - - - enregistrer_etude_en_cours - - - - - fermer_etude_en_cours - - - - - quitter_application - - - - - lancer_solveur - - - - - interrompt_simulation_en_cours - - - - - lancer_mailleur_externe - - - - - afficher_listings_simulation - - - - - Aficher les listings de la simulation courante - - - - - Maillage - - - - - Cond. Limites - - - - - Frottements - - - - - Ouvrages - + + Define initial conditions + Définire les conditions initiales - + Édition des Tronçons - + Ouvrir une étude - + Enrégistrer étude en cours (Ctrl+S) - + Fermer étude en cours (Ctrl+F) - - - &Réseau + + + MainWindowProfile + + + Profile + + + + + (no name) + + + + + Quittez ? + + + + + MainWindowProfile + + + + + Trier les points par ordre croissant de X + + + + + Trier les points par ordre croissant de Y + + + + + Nom + + + + + Abs en travers (m) + + + + + La cote du fond + Z minimale + + + + + La cote maximale + Z maximale + + + + + Rive gauche + + + + + Rive droite + + + + + Abscisse en travers (m) + + + + + Cote (m) - - &Géométrie + + Suppression les lignes incomplètes - - &Exécuter + + Supprimer les lignes des cellules non renseignées ? - - Sous-étude Rubar3 + + Suppression des noms répétés - - Nouvelle étude MAGE + + Etes-vous sûr de vouloir quitter ? - - Nouvelle étude RubarBE + + Insérer un point - - Importer un jeu de données MAGE + + Supprimer le/les point(s) sélectionnés - - Importer un jeu de données RubarBE + + Trier les points par ordre décroissant de X - - Éditer le réseau + + Trier les points par ordre décroissant de Y - - Éditer la géométrie + + Décaler le point sélectionné vers le haut - - Importer une géométrie + + Décaler le point sélectionné vers le bas - - Exporter la géométrie + + Exporter (dans un fichier) les points du profil au format tabulé - - Visualiser la géométrie maillée + + Copier la sélection au format tabulé - - Paramètres numériques du solveur MAGE + + Coller la sélection depuis le presse-papier au format tabulé - - Activer/Désactiver l'export des conditions initiales + + Vérifier la validité de la saisie et garder ou pas les modifications apportées - - Importer l'état final comme état initial + + Annuler toutes les modifications depuis la dernière validation - - Édition des Frottements + + Annuler toutes les modifications et revenir à l'état initial - - Édition des Apports Latéraux + + Ligne d'eau : + Z : Cote (m) + A : Aire mouillée (mu00B2) + p : Périmètre mouillé (m) + L : Largeur au miroir (m) - - Édition des déversements + + 'Maj + Clic' : Ligne d'eau & 'Ctrl + Clic' : Sélectionner des points - - Édition des ouvrages en travers + + Abscisse en travers calculée en projétant les points +sur le plan défini par les deux points nommés extrêmes + + + + + MainWindow_reach + + + Stricklers + + + + + Kp (m) + Pk (m) + + + + Ouvrir un fichier + + + + + Fichiers .ST (*.ST) + + + + + Fichiers .M (*.M) + + + + + Tous les fichiers (*) + + + + + Kp : + + + + + Files .ST(*.ST or *.st) + + + + + All files (*) + + + + + Abscisse en travers (m) + + + + + Cote (m) + + + + + Profil suivant + + + + + Jeu de sections du Bief + + + + + +Ordre des sections : Amont --> Aval + + + + + Pk = + + + + + Nouveau profil + + + + + Trier les profils par ordre croissant des Pk + + + + + Name + Nom + + + + Type + Type + + + + Alt+Z + + + + + Alt+E + + + + + Alt+R + + + + + Vue globale automatique (Alt+S) + + + + + Vue globale automatique (Alt+D) + + + + + Vue globale automatique (Alt+F) + + + + + X (m) + + + + + Y (m) + + + + + Draft (m) + + + + + KP (m) + + + + + Discharge (m^3/s) + Débit (m³/s) + + + + Choisissez un nom de fichier à sauvegarder - - Gestion des répertoires de simulation + + Édition des profils sélectionnés - - Vitesse(Pk) à t fixé + + Vous avez sélectionné plus de 5 profils. +Seuls les 5 premiers seront édités. - - Vitesse(t) à Pk fixé + + Profil N° : - - Charge hydraulique(Pk) à t fixé + + Profil précédent - - Charge hydraulique(t) à Pk fixé + + Profil sélectionné - - Autres résulats MAGE + + Importer une géométrie - - À propos + + Supprimer le profil sélectionné - - Lancer le solveur pour réaliser une simulation + + Éditer le profil sélectionné - - Lancer le mailleur externe sur la géométrie du bief courant + + Copier le profil sélectionné - - Réseau + + Coller le profil en fin de liste (penser à modifier le Pk avant de trier) - - Ouvrir l'éditeur de la topologie du réseau + + Dupliquer la section sélectionnée - - Géométrie + + Trier les profils par ordre décroissant des Pk - - Ouvrir l'éditeur de géométrie + + Changer l'ordre des profils (en décalant le profil sélectionné vers le haut) - - Ouvir l'éditeur des Conditions aux Limites & Apports Ponctuels + + Changer l'ordre des profils (en décalant le profil sélectionné vers le bas) - - App. Latéraux + + Terminer l'édition - - Ouvrir l'éditeur des Apports Latéraux Distribués + + Vue isométrique (Alt+Z) - - Déversements + + Vue isométrique (Alt+E) - - Ouvrir l'éditeur des Déversements Latéraux + + Vue isométrique (Alt+R) + + + + + Network + + + Add node + Ajouter un nœud + + + + Delete the node + Supprimer un nœud + + + + Disable the node + Déactiver un nœud + + + + Delete the reach + Supprimer un bief + + + + Disable the reach + Déactiver un bief + + + + Enable the reach + Activer un bief + + + + Reverse the reach orientation + Inverser l'orientation du bief + + + + SolverParameters + + + Name + Nom + + + + Value + Valeur + + + + Yes + Oui + + + + No + Non + + + + Y + O + + + + N + N + + + + Initial time (jj:hh:mm:ss) + Temps initial (jj:hh:mm:ss) + + + + Final time (jj:hh:mm:ss) + Temps final (jj:hh:mm:ss) + + + + Timestep (second) + Pas de temps (en second) + + + + Minimum timestep (second) + Pas de temps minimal (en second) + + + + Time step of writing on .TRA + Pas de temps d'écriture dans le fichier .TRA + + + + Time step of writing on .BIN + Pas de temps d'écriture dans le fichier .BIN + + + + Implicitation parameter + + + + + Continuity discretization type (S/L) + + + + + QSJ discretization (A/B) + + + + + Stop criterion iterations (G/A/R) + + + + + Iteration type + + + + + Smoothing coefficient + + + + + Maximun accepted number of CFL + + + + + Minimum water height (meter) + + + + + Maximun number of iterations (< 100) + + + + + Timestep reduction factor + + + + + Reduction precision factor of Z + + + + + Reduction precision factor of Q + + + + + Reduction precision factor of residue + + + + + Number of iteration at maximum precision + + + + + Number of iteration before switch + + + + + Maximum accepted Froude number + + + + + Diffluence node height balance + + + + + Compute reach volume balance (Y/N) + + + + + Maximum reach volume balance + + + + + Minimum reach volume to check + + + + + Solvers + + + Generic + Générique + + + + Mage version 8 + Mage en version 8 + + + + Toolbar + + + Vue originale + + + + + Panoramique des axes avec la souris gauche, zoom avec la droite + + + + + Zoom + + + + + Vue globale automatique (Shift+X) + + + + + Enregistrer la figure - - Tronçons + + Retour à la vue précédente - - Ouvrir l'éditeur des tronçons pour les frottements et Apports Latéraux + + Passer à la vue suivante - - Ouvrir l'éditeur des frottements au fond - - - - - Ouvrir l'éditeur des ouvrages (seuils, vannes, etc.), singularités et pompes + + Vue isométrique (Shift+W) From 25a01682d400c83636fbce7ae76cd9c4022e20e7 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 17:03:18 +0200 Subject: [PATCH 086/289] ci: Enable windows package. --- .gitlab-ci.yml | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ec641795..da4feb5c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -55,23 +55,23 @@ linux-package: - cd packages - ./linux.sh -# windows-package: -# stage: package -# tags: -# - release -# - wine -# needs: -# - job: build-lang -# artifacts: true -# # rules: -# # - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG -# # when: never -# artifacts: -# paths: -# - packages/pamhyr-win-amd64.exe -# script: -# - cd packages -# - ./windows.bat +windows-package: + stage: package + tags: + - release + - wine + needs: + - job: build-lang + artifacts: true + # rules: + # - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG + # when: never + artifacts: + paths: + - packages/pamhyr-win-amd64.exe + script: + - cd packages + - ./windows.bat ########### # RELEASE # @@ -85,8 +85,8 @@ tag-release: needs: - job: linux-package artifacts: true - # - job: windows-package - # artifacts: true + - job: windows-package + artifacts: true rules: - if: $CI_COMMIT_TAG artifacts: @@ -108,7 +108,7 @@ tag-release: url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz' filepath: '/packages/pamhyr-gnulinux-amd64.tar.xz' link_type: 'other' - # - name: 'Windows amd64 (exe)' - # url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' - # filepath: '/packages/pamhyr-win-amd64.exe' - # link_type: 'other' + - name: 'Windows amd64 (exe)' + url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' + filepath: '/packages/pamhyr-win-amd64.exe' + link_type: 'other' From 86d022dc9368df087502c33436792db62ab3e609 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 26 Jul 2023 17:14:05 +0200 Subject: [PATCH 087/289] ci: Active rules for packages. --- .gitlab-ci.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index da4feb5c..5bb9624c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,9 +45,8 @@ linux-package: needs: - job: build-lang artifacts: true - # rules: - # - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG - # when: never + rules: + - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG artifacts: paths: - packages/pamhyr-gnulinux-amd64.tar.xz @@ -63,9 +62,8 @@ windows-package: needs: - job: build-lang artifacts: true - # rules: - # - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG - # when: never + rules: + - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG artifacts: paths: - packages/pamhyr-win-amd64.exe @@ -95,8 +93,6 @@ tag-release: - packages/pamhyr-win-amd64.exe script: - cd packages - # - ./linux.sh - # # - ./wine.sh ci release: name: '$CI_COMMIT_TAG' description: 'Automatic release from tag $CI_COMMIT_TAG' From a22a7606100874273076c7aec9e73971c468890c Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 28 Jul 2023 09:56:47 +0200 Subject: [PATCH 088/289] About: Update about window and add VERSION file. --- .gitlab-ci.yml | 12 +++- VERSION | 1 + packages/linux.sh | 1 + packages/version.sh | 10 +++ packages/windows.bat | 1 + src/VERSION | 1 + src/View/ASubWindow.py | 25 +++++++ src/View/About/Window.py | 20 ++++++ src/View/ui/about.ui | 92 +++++++++++++++++--------- src/View/ui/ressources/Logo-INRAE.png | Bin 0 -> 8635 bytes 10 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 VERSION create mode 100755 packages/version.sh create mode 120000 src/VERSION create mode 100644 src/View/ui/ressources/Logo-INRAE.png diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5bb9624c..014dde47 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,8 +10,14 @@ stages: build: stage: build + tags: + - linux script: - - echo "TODO build pamhyr" + - cd packages + - ./version.sh "$CI_COMMIT_BRANCH" "$CI_COMMIT_TAG" "$CI_COMMIT_SHORT_SHA" + artifacts: + paths: + - VERSION build-lang: stage: build @@ -45,6 +51,8 @@ linux-package: needs: - job: build-lang artifacts: true + - job: build + artifacts: true rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG artifacts: @@ -62,6 +70,8 @@ windows-package: needs: - job: build-lang artifacts: true + - job: build + artifacts: true rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG artifacts: diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..5664e303 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +git diff --git a/packages/linux.sh b/packages/linux.sh index 1cd72d28..9924eb0b 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -35,6 +35,7 @@ cp -r ../src/View/ui/*.ui dist/pamhyr/View/ui/ mkdir -p dist/pamhyr/lang cp -r ../src/lang/*.qm dist/pamhyr/lang/ +cp ../VERSION dist/pamhyr/ echo " *** MAKE SRC PACKAGE" diff --git a/packages/version.sh b/packages/version.sh new file mode 100755 index 00000000..aa769b83 --- /dev/null +++ b/packages/version.sh @@ -0,0 +1,10 @@ +#! /bin/sh + +# ./version BRANCH TAG COMMIT + +if [ -z $2 ]; +then + echo "$1-$3" > ../VERSION +else + echo "$2" > ../VERSION +fi diff --git a/packages/windows.bat b/packages/windows.bat index 42c46ba0..c5d150df 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -15,6 +15,7 @@ copy /y ..\src\View\ui\ressources\ dist\pamhyr\View\ui\ressources copy /y ..\src\View\ui\Widgets\*.ui dist\pamhyr\View\ui\Widgets copy /y ..\src\View\ui\*.ui dist\pamhyr\View\ui\ copy /y ..\src\lang\*.qm dist\pamhyr\lang\ +copy /y ..\VERSION dist\pamhyr\ rem Make installer "C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi diff --git a/src/VERSION b/src/VERSION new file mode 120000 index 00000000..6ff19de4 --- /dev/null +++ b/src/VERSION @@ -0,0 +1 @@ +../VERSION \ No newline at end of file diff --git a/src/View/ASubWindow.py b/src/View/ASubWindow.py index 478993cf..17ae4a07 100644 --- a/src/View/ASubWindow.py +++ b/src/View/ASubWindow.py @@ -19,6 +19,7 @@ from PyQt5.QtWidgets import ( QRadioButton, QComboBox, QFileDialog, QMessageBox, QTableView, QAction, QDateTimeEdit, QWidget, QPlainTextEdit, + QLabel, ) from PyQt5.QtCore import ( QTime, QDateTime, @@ -130,6 +131,30 @@ class ASubWindowFeatures(object): return qtype + def get_label_text(self, name:str): + """Get text of label component + + Args: + label: The label component name + + Returns: + Text + """ + return self.find(QLabel, name).text() + + def set_label_text(self, name:str, text:str): + """Set text of label component + + Args: + text_edit: The label component name + text: The text + + Returns: + Nothing + """ + self.find(QLabel, name).setText(text) + + def set_line_edit_text(self, name:str, text:str): """Set text of line edit component diff --git a/src/View/About/Window.py b/src/View/About/Window.py index 9f84ad86..2ff5f3ce 100644 --- a/src/View/About/Window.py +++ b/src/View/About/Window.py @@ -1,8 +1,28 @@ # -*- coding: utf-8 -*- +import os +import logging + from View.ASubWindow import ASubWindow +logger = logging.getLogger() + class AboutWindow(ASubWindow): def __init__(self, title="About", parent=None): super(AboutWindow, self).__init__(name=title, ui="about", parent=parent) self.ui.setWindowTitle(title) + + with open( + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "VERSION" + ) + ), "r" + ) as f: + version = f.readline() + logger.info(version) + + label = self.get_label_text("label_version") + label = label.replace("@version", version.strip()) + self.set_label_text("label_version", label) diff --git a/src/View/ui/about.ui b/src/View/ui/about.ui index 6b4f6957..25d11617 100644 --- a/src/View/ui/about.ui +++ b/src/View/ui/about.ui @@ -6,8 +6,8 @@ 0 0 - 362 - 98 + 553 + 262 @@ -15,44 +15,74 @@ - + - + - ressources/logoCemagref.gif - - - true + ressources/Logo-INRAE.png - - - - - - 16 - 75 - true - - - - PamHyr - - - - - - - Version en developpement: - - - - + + + + 16 + 75 + true + + + + About PAMHYR + + + + + + + © Pierre-Antoine ROUBY - INRAE -2023 + + + + + + + Version: @version + + + + + + + License: GPLv3+ + + + + + + + <a href="https://gitlab.irstea.fr/theophile.terraz/pamhyr">Source code</a> + + + Qt::RichText + + + + + + + Qt::Vertical + + + + 20 + 40 + + + diff --git a/src/View/ui/ressources/Logo-INRAE.png b/src/View/ui/ressources/Logo-INRAE.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd93142e9ad2127cf167eac4acf97afed6be74b GIT binary patch literal 8635 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 literal 0 HcmV?d00001 From 0a87261d94484c1946bacbbcbe7e86243a824cf6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 28 Jul 2023 10:54:17 +0200 Subject: [PATCH 089/289] MainWindow: Diable unused help action. --- src/View/ui/MainWindow.ui | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 6913d02a..217bd2b4 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -627,11 +627,17 @@ + + false + Help PAMHYR + + false + Help MAGE From 02394cc2265d4aec31a47d50854822c11daf6704 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 28 Jul 2023 11:14:18 +0200 Subject: [PATCH 090/289] Trans: Update french translate. --- src/View/ui/about.ui | 3 + src/lang/fr.ts | 156 ++++++++++++++++++++++--------------------- 2 files changed, 84 insertions(+), 75 deletions(-) diff --git a/src/View/ui/about.ui b/src/View/ui/about.ui index 25d11617..7d9ab7b6 100644 --- a/src/View/ui/about.ui +++ b/src/View/ui/about.ui @@ -13,6 +13,9 @@ Form + + + diff --git a/src/lang/fr.ts b/src/lang/fr.ts index 9eabd6ef..63008b40 100644 --- a/src/lang/fr.ts +++ b/src/lang/fr.ts @@ -1,5 +1,6 @@ - + + BoundaryCondition @@ -83,14 +84,9 @@ Nœud - - Discharge (m³/s) - Débit (m³/s) - - Discharge (m³/s) - + Débit (m³/s) @@ -276,7 +272,7 @@ Stricklers - + Stricklers @@ -454,16 +450,6 @@ Form - - - PamHyr - - - - - Version en developpement: - - dd/MM/yyyy HH:mm:ss @@ -472,13 +458,38 @@ days - jours + jours HH:mm:ss + + + About PAMHYR + À propos de PAMHYR + + + + © Pierre-Antoine ROUBY - INRAE -2023 + + + + + Version: @version + + + + + License: GPLv3+ + Licence: GPLv3+ + + + + <a href="https://gitlab.irstea.fr/theophile.terraz/pamhyr">Source code</a> + <a href="https://gitlab.irstea.fr/theophile.terraz/pamhyr">Code source</a> + Frictions @@ -659,14 +670,9 @@ Hauteur (m) - - Discharge (m³/s) - Débit (m³/s) - - Discharge (m³/s) - + Débit (m³/s) @@ -1371,28 +1377,28 @@ Define initial conditions Définire les conditions initiales - + Édition des Tronçons - + Ouvrir une étude - + Enrégistrer étude en cours (Ctrl+S) - + Fermer étude en cours (Ctrl+F) - + MainWindowProfile @@ -1466,101 +1472,101 @@ Cote (m) - + Suppression les lignes incomplètes - + Supprimer les lignes des cellules non renseignées ? - + Suppression des noms répétés - + Etes-vous sûr de vouloir quitter ? - + Insérer un point - + Supprimer le/les point(s) sélectionnés - + Trier les points par ordre décroissant de X - + Trier les points par ordre décroissant de Y - + Décaler le point sélectionné vers le haut - + Décaler le point sélectionné vers le bas - + Exporter (dans un fichier) les points du profil au format tabulé - + Copier la sélection au format tabulé - + Coller la sélection depuis le presse-papier au format tabulé - + Vérifier la validité de la saisie et garder ou pas les modifications apportées - + Annuler toutes les modifications depuis la dernière validation - + Annuler toutes les modifications et revenir à l'état initial - + Ligne d'eau : - Z : Cote (m) - A : Aire mouillée (mu00B2) - p : Périmètre mouillé (m) - L : Largeur au miroir (m) + Z : Cote (m) + A : Aire mouillée (mu00B2) + p : Périmètre mouillé (m) + L : Largeur au miroir (m) - + 'Maj + Clic' : Ligne d'eau & 'Ctrl + Clic' : Sélectionner des points - + Abscisse en travers calculée en projétant les points sur le plan défini par les deux points nommés extrêmes @@ -1720,98 +1726,98 @@ Ordre des sections : Amont --> Aval Discharge (m^3/s) Débit (m³/s) - + Choisissez un nom de fichier à sauvegarder - + Édition des profils sélectionnés - + Vous avez sélectionné plus de 5 profils. Seuls les 5 premiers seront édités. - + Profil N° : - + Profil précédent - + Profil sélectionné - + Importer une géométrie - + Supprimer le profil sélectionné - + Éditer le profil sélectionné - + Copier le profil sélectionné - + Coller le profil en fin de liste (penser à modifier le Pk avant de trier) - + Dupliquer la section sélectionnée - + Trier les profils par ordre décroissant des Pk - + Changer l'ordre des profils (en décalant le profil sélectionné vers le haut) - + Changer l'ordre des profils (en décalant le profil sélectionné vers le bas) - + Terminer l'édition - + Vue isométrique (Alt+Z) - + Vue isométrique (Alt+E) - + Vue isométrique (Alt+R) @@ -2058,17 +2064,17 @@ Seuls les 5 premiers seront édités. Enregistrer la figure - + Retour à la vue précédente - + Passer à la vue suivante - + Vue isométrique (Shift+W) From d636f5b637c0f1b97c30660cb257cac0839eb513 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 28 Jul 2023 11:48:00 +0200 Subject: [PATCH 091/289] SL: Reach: Profile: Add KP when profile has no name. --- src/View/SedimentLayers/Reach/Profile/Window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index 93b201a6..d1dd5e69 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -64,7 +64,10 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): pname = self._profile.name if pname == "": - pname = _translate("SedimentLayers", "(no name)") + pname = _translate( + "SedimentLayers", + "(no name - @kp)").replace("@kp", str(self._profile.kp) + ) self._title = ( title + " - " From a5a712d6ad7f66c77cce2cfbf74cbfef29cd8e48 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 10:17:44 +0200 Subject: [PATCH 092/289] About: Add AUTHORS files and authors list into about window. --- AUTHORS | 3 +++ packages/linux.sh | 1 + packages/windows.bat | 1 + src/AUTHORS | 1 + src/View/About/Window.py | 36 +++++++++++++++++++++++++++--------- src/View/ui/about.ui | 2 +- 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 AUTHORS create mode 120000 src/AUTHORS diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..0ce9cdac --- /dev/null +++ b/AUTHORS @@ -0,0 +1,3 @@ +Sylvain COULIBALY, INRAE, 2020 - 2022 +Théophile TERRAZ, INRAE, 2022 - 2023 +Pierre-Antoine ROUBY, INRAE, 2023 \ No newline at end of file diff --git a/packages/linux.sh b/packages/linux.sh index 9924eb0b..938e4ff9 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -36,6 +36,7 @@ mkdir -p dist/pamhyr/lang cp -r ../src/lang/*.qm dist/pamhyr/lang/ cp ../VERSION dist/pamhyr/ +cp ../AUTHORS dist/pamhyr/ echo " *** MAKE SRC PACKAGE" diff --git a/packages/windows.bat b/packages/windows.bat index c5d150df..84d55613 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -16,6 +16,7 @@ copy /y ..\src\View\ui\Widgets\*.ui dist\pamhyr\View\ui\Widgets copy /y ..\src\View\ui\*.ui dist\pamhyr\View\ui\ copy /y ..\src\lang\*.qm dist\pamhyr\lang\ copy /y ..\VERSION dist\pamhyr\ +copy /y ..\AUTHORS dist\pamhyr\ rem Make installer "C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi diff --git a/src/AUTHORS b/src/AUTHORS new file mode 120000 index 00000000..9eadf712 --- /dev/null +++ b/src/AUTHORS @@ -0,0 +1 @@ +../AUTHORS \ No newline at end of file diff --git a/src/View/About/Window.py b/src/View/About/Window.py index 2ff5f3ce..a1784509 100644 --- a/src/View/About/Window.py +++ b/src/View/About/Window.py @@ -5,24 +5,42 @@ import logging from View.ASubWindow import ASubWindow +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate logger = logging.getLogger() class AboutWindow(ASubWindow): + def _path_file(self, filename): + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", filename + ) + ) + + def __init__(self, title="About", parent=None): super(AboutWindow, self).__init__(name=title, ui="about", parent=parent) self.ui.setWindowTitle(title) - with open( - os.path.abspath( - os.path.join( - os.path.dirname(__file__), - "..", "..", "VERSION" - ) - ), "r" - ) as f: + # Version + with open(self._path_file("VERSION"), "r") as f: version = f.readline() - logger.info(version) + logger.info(f"version: {version}") label = self.get_label_text("label_version") label = label.replace("@version", version.strip()) self.set_label_text("label_version", label) + + # Authors + with open(self._path_file("AUTHORS"), "r") as f: + label = "" + try: + while True: + author = next(f).strip() + logger.info(f"author: {author}") + label = f"\n © {author}" + label + except StopIteration: + label = _translate("About", "Contributors: ") + label + self.set_label_text("label_copyright", label) diff --git a/src/View/ui/about.ui b/src/View/ui/about.ui index 7d9ab7b6..579e0095 100644 --- a/src/View/ui/about.ui +++ b/src/View/ui/about.ui @@ -46,7 +46,7 @@ - © Pierre-Antoine ROUBY - INRAE -2023 + ... From 5f0a915ac6d20325fec678ce1331c2cb8ca93252 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 10:29:02 +0200 Subject: [PATCH 093/289] Network: Draw reach as black color when is current reach. --- src/View/Network/GraphWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index 2569497a..737ca6ba 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -158,7 +158,7 @@ class EdgeItem(QGraphicsItem): if self.graph.selected_item() == self: color = Qt.red elif self.graph.current_edge() == self: - color = Qt.blue + color = Qt.black elif not self.graph.graph.is_enable_edge(self.edge): color = Qt.darkGray From 75dba77dec09b37a9e201af4ae9e3fee073a1643 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 10:30:58 +0200 Subject: [PATCH 094/289] Trans: Minor change. --- src/lang/fr.ts | 116 ++++++++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/src/lang/fr.ts b/src/lang/fr.ts index 63008b40..07808a3c 100644 --- a/src/lang/fr.ts +++ b/src/lang/fr.ts @@ -1,6 +1,14 @@ + + About + + + Contributors: + Contributeurs : + + BoundaryCondition @@ -466,30 +474,30 @@ - + About PAMHYR À propos de PAMHYR - - © Pierre-Antoine ROUBY - INRAE -2023 - - - - + Version: @version - + License: GPLv3+ Licence: GPLv3+ - + <a href="https://gitlab.irstea.fr/theophile.terraz/pamhyr">Source code</a> <a href="https://gitlab.irstea.fr/theophile.terraz/pamhyr">Code source</a> + + + ... + + Frictions @@ -788,7 +796,7 @@ Fermer - + Close current study Fermer l'étude en cours @@ -828,7 +836,7 @@ Configuration de PAMHYR - + Quit Quitter @@ -838,7 +846,7 @@ - + Edit river network Éditer le réseau @@ -908,7 +916,7 @@ Condition aux limites et apports ponctuels - + Initial conditions Conditions initiales @@ -943,7 +951,7 @@ Éditer les ouvrages - + Run solver Lancer un solver @@ -988,32 +996,32 @@ Cartographier le bief sélectionné - + Help PAMHYR Aide de PAMHYR - + Help MAGE Aide de MAGE - + About A propos - + ouvrir - + Save current study Sauvegarder l'étude - + Ctrl+F @@ -1023,132 +1031,132 @@ Quitter l'application - + Ctrl+Q - + stop solver - + Interrompt la simulation en cours - + Ctrl+C - + Run external meshing tool Lancer le mailler externe - + Run meshing tool on current reach geometry Lancer le mailler externe sur le bief selectionné - + Display simulation listing - + Display current simulation listing - + River network Réseau - + Geometry Géométrie - + Edit reach geometry Éditer la géométrie - + Mesh Maillage - + Boundary conditions Conditions aux limites - + Edit boundary conditions and one-time contributions Éditer les conditions aux limites et les apports ponctuels - + Lateral contribution Contributions latérales - + Edit lateral contribution Éditer les contributions latérales - + Spills - + Edit lateral spills - + Friction Frottements - + Stricklers - + Edit the study stricklers Éditer les Stricklers de l'étude - + Building Ouvrages - + Edit building (valve, ...), singularity and pump Éditer les ouvrages - + Edit study Éditer l'étude - + English - + French @@ -1353,27 +1361,27 @@ Trier les points des conditions aux limites - + Quit the application (Ctrl+Q) - + Run a solver Lancer un solver - + Display meshed reach - + Edit friction frictions Éditer les frottements - + Define initial conditions Définire les conditions initiales @@ -1383,17 +1391,17 @@ - + Ouvrir une étude - + Enrégistrer étude en cours (Ctrl+S) - + Fermer étude en cours (Ctrl+F) From e4df36b5f3fc06c4fa3ba00c6902aba12b3ada64 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 10:40:30 +0200 Subject: [PATCH 095/289] About: Minor change. --- src/View/About/Window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/View/About/Window.py b/src/View/About/Window.py index a1784509..f4a98121 100644 --- a/src/View/About/Window.py +++ b/src/View/About/Window.py @@ -26,11 +26,11 @@ class AboutWindow(ASubWindow): # Version with open(self._path_file("VERSION"), "r") as f: - version = f.readline() + version = f.readline().strip() logger.info(f"version: {version}") label = self.get_label_text("label_version") - label = label.replace("@version", version.strip()) + label = label.replace("@version", version) self.set_label_text("label_version", label) # Authors From 61f0ba7b1827a3c5882da32c1179f1208071e190 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 10:40:41 +0200 Subject: [PATCH 096/289] Packages: wine: Add version/authors files copy. --- packages/wine.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/wine.sh b/packages/wine.sh index 1a769f51..07c43b6b 100755 --- a/packages/wine.sh +++ b/packages/wine.sh @@ -70,6 +70,9 @@ cp -r ../src/View/ui/ressources/ dist/pamhyr/View/ui/ cp -r ../src/View/ui/Widgets/*.ui dist/pamhyr/View/ui/ cp -r ../src/View/ui/*.ui dist/pamhyr/View/ui/ +cp ../VERSION dist/pamhyr/ +cp ../AUTHORS dist/pamhyr/ + # Update TS and build QM files OLD_PWD=$PWD cd ../src/lang/ From 334e7b58222fdcbb21d8d52d192d2d3ee399a49a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 10:43:50 +0200 Subject: [PATCH 097/289] Network: Remove "enable" column from reach table. --- src/View/Network/Table.py | 12 ++++++------ src/View/Network/Window.py | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/View/Network/Table.py b/src/View/Network/Table.py index a8254745..4dad50f5 100644 --- a/src/View/Network/Table.py +++ b/src/View/Network/Table.py @@ -148,12 +148,12 @@ class GraphTableModel(QAbstractTableModel): node ) ) - elif self.headers[index.column()] == "enable": - self._undo.push( - EnableEdgeCommand( - self.rows[index.row()], value - ) - ) + # elif self.headers[index.column()] == "enable": + # self._undo.push( + # EnableEdgeCommand( + # self.rows[index.row()], value + # ) + # ) else: self._undo.push( SetCommand( diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index 8fdf3988..19a5d856 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -71,7 +71,8 @@ class NetworkWindow(ASubMainWindow): # Edges table self._reachs_model = GraphTableModel( - headers = ["name", "enable", "node1", "node2"], + headers = ["name", # "enable", + "node1", "node2"], graph = self._graph, rows_type = "edges", undo = self._undo_stack, @@ -86,9 +87,9 @@ class NetworkWindow(ASubMainWindow): table = self.find(QTableView, "tableView_reachs") table.setModel(self._reachs_model) - table.setItemDelegateForColumn(1, self.delegate_true_false_combobox) + # table.setItemDelegateForColumn(1, self.delegate_true_false_combobox) + table.setItemDelegateForColumn(1, self.delegate_combobox) table.setItemDelegateForColumn(2, self.delegate_combobox) - table.setItemDelegateForColumn(3, self.delegate_combobox) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) #table.resizeColumnsToContents() From a3f3fca6b629dacea4791ce9350868ec66ac8023 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 11:05:11 +0200 Subject: [PATCH 098/289] tools: ssh: Minor change on ssh run tool script. --- tools/ssh-run.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/ssh-run.sh b/tools/ssh-run.sh index 9c59bb9f..a7ca1af7 100755 --- a/tools/ssh-run.sh +++ b/tools/ssh-run.sh @@ -1,10 +1,14 @@ #! /bin/sh -# > ssh-run.sh SERVER DESTDIR SOLVER INPUT +# > ssh-run.sh SERVER DESTDIR SOLVER ARGS INPUT # First argument is the server name/addr # The second argument is the destination directory to copy input data # The third argument is the solver path -# The fourth argument is the input name +# The fourth argument is an solver args separate by ',' or input name +# The sixth argument is the input name or nothing -ssh $1 "cd $2; $3 $4" +args=$(echo $4 | tr ',' ' ') + +echo "ssh $1 \"cd $2; $3 $args $5\"" +ssh $1 "cd $2; $3 $args $5" From 216bb3dd4cad484c08f048891de017592bb04b01 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 11:25:06 +0200 Subject: [PATCH 099/289] git: Add submodule mage. --- .gitlab-ci.yml | 23 +++++++++++++++++++++++ .gitmodules | 3 +++ mage | 1 + packages/linux.sh | 4 ++++ 4 files changed, 31 insertions(+) create mode 100644 .gitmodules create mode 160000 mage diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 014dde47..183337a4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,27 @@ stages: # BUILD # ######### +build-mage: + stage: build + tags: + - linux + rules: + - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG + variables: + GIT_SUBMODULE_STRATEGY: recursive + GIT_SUBMODULE_DEPTH: 1 + script: + - cd mage/src/ + - mkdir -p build + - cd build + - cmake .. + - make + artifacts: + paths: + - mage/src/build/mage + - mage/src/build/mage_extraire + - mage/src/build/mailleurPF + build: stage: build tags: @@ -53,6 +74,8 @@ linux-package: artifacts: true - job: build artifacts: true + - job: build-mage + artifacts: true rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG artifacts: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..0aad7c1d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mage"] + path = mage + url = https://gitlab.irstea.fr/jean-baptiste.faure/mage.git diff --git a/mage b/mage new file mode 160000 index 00000000..18aa9a7e --- /dev/null +++ b/mage @@ -0,0 +1 @@ +Subproject commit 18aa9a7e0f8415a94a40fe647d329268c6932663 diff --git a/packages/linux.sh b/packages/linux.sh index 938e4ff9..899aa4b8 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -38,6 +38,10 @@ cp -r ../src/lang/*.qm dist/pamhyr/lang/ cp ../VERSION dist/pamhyr/ cp ../AUTHORS dist/pamhyr/ +cp ../mage/src/build/mage dist/pamhyr/ +cp ../mage/src/build/mage_extraire dist/pamhyr/ +cp ../mage/src/build/mailleurPF dist/pamhyr/ + echo " *** MAKE SRC PACKAGE" OLD_PWD=$PWD From 099ed29d0675a2d50e39e3d97a4a61ac11a5dbde Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 31 Jul 2023 15:16:19 +0200 Subject: [PATCH 100/289] ci: Add MAGE build for windows. --- .gitlab-ci.yml | 31 +++++++++++++++++++++++++++++-- packages/windows.bat | 10 ++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 183337a4..87578db8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,7 @@ stages: # BUILD # ######### -build-mage: +build-mage-linux: stage: build tags: - linux @@ -29,6 +29,31 @@ build-mage: - mage/src/build/mage_extraire - mage/src/build/mailleurPF +build-mage-windows: + stage: build + tags: + - wine + rules: + - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG + variables: + GIT_SUBMODULE_STRATEGY: recursive + GIT_SUBMODULE_DEPTH: 1 + script: + - cd mage/src/ + - sed s/add_compile_definitions(win)/add_compile_definitions(windows)/ CMakeLists.txt > __tmp__ + - echo set(CMAKE_Fortran_COMPILER "C:/Program\ Files/gcc/bin/gfortran.exe") > CMakeLists.txt + - echo set(CMAKE_MAKE_PROGRAM "C:/Program Files\ \(x86\)/GnuWin32/bin/make.exe") >> CMakeLists.txt + - type __tmp__ >> CMakeLists.txt + - mkdir build + - cd build + - cmake -G "MinGW Makefiles" .. + - make + artifacts: + paths: + - mage/src/build/mage.exe + - mage/src/build/mage_extraire.exe + - mage/src/build/mailleurPF.exe + build: stage: build tags: @@ -74,7 +99,7 @@ linux-package: artifacts: true - job: build artifacts: true - - job: build-mage + - job: build-mage-linux artifacts: true rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG @@ -95,6 +120,8 @@ windows-package: artifacts: true - job: build artifacts: true + - job: build-mage-windows + artifacts: true rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG artifacts: diff --git a/packages/windows.bat b/packages/windows.bat index 84d55613..453b0d03 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -11,12 +11,22 @@ rem Copy data mkdir dist\pamhyr\View\ui\ressources mkdir dist\pamhyr\View\ui\Widgets +rem UI copy /y ..\src\View\ui\ressources\ dist\pamhyr\View\ui\ressources copy /y ..\src\View\ui\Widgets\*.ui dist\pamhyr\View\ui\Widgets copy /y ..\src\View\ui\*.ui dist\pamhyr\View\ui\ + +rem Lang copy /y ..\src\lang\*.qm dist\pamhyr\lang\ + +rem Information copy /y ..\VERSION dist\pamhyr\ copy /y ..\AUTHORS dist\pamhyr\ +rem MAGE +copy /y ..\mage\src\build\mage.exe dist\pamhyr\ +copy /y ..\mage\src\build\mage_extraire.exe dist\pamhyr\ +copy /y ..\mage\src\build\mailleurPF.exe dist\pamhyr\ + rem Make installer "C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi From 3d4750217975be847eb4f5eb3a2059566b2fb010 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 1 Aug 2023 11:08:28 +0200 Subject: [PATCH 101/289] ci: Disable mage build for windows. --- .gitlab-ci.yml | 54 ++++++++++++++++++++++---------------------- packages/windows.bat | 6 ++--- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 87578db8..2cbfbd42 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,7 @@ build-mage-linux: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG variables: GIT_SUBMODULE_STRATEGY: recursive - GIT_SUBMODULE_DEPTH: 1 + GIT_SUBMODULE_DEPTH: 20 script: - cd mage/src/ - mkdir -p build @@ -29,30 +29,30 @@ build-mage-linux: - mage/src/build/mage_extraire - mage/src/build/mailleurPF -build-mage-windows: - stage: build - tags: - - wine - rules: - - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG - variables: - GIT_SUBMODULE_STRATEGY: recursive - GIT_SUBMODULE_DEPTH: 1 - script: - - cd mage/src/ - - sed s/add_compile_definitions(win)/add_compile_definitions(windows)/ CMakeLists.txt > __tmp__ - - echo set(CMAKE_Fortran_COMPILER "C:/Program\ Files/gcc/bin/gfortran.exe") > CMakeLists.txt - - echo set(CMAKE_MAKE_PROGRAM "C:/Program Files\ \(x86\)/GnuWin32/bin/make.exe") >> CMakeLists.txt - - type __tmp__ >> CMakeLists.txt - - mkdir build - - cd build - - cmake -G "MinGW Makefiles" .. - - make - artifacts: - paths: - - mage/src/build/mage.exe - - mage/src/build/mage_extraire.exe - - mage/src/build/mailleurPF.exe +# build-mage-windows: +# stage: build +# tags: +# - wine +# rules: +# - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG +# variables: +# GIT_SUBMODULE_STRATEGY: recursive +# GIT_SUBMODULE_DEPTH: 20 +# script: +# - cd mage/src/ +# - sed s/add_compile_definitions(win)/add_compile_definitions(windows)/ CMakeLists.txt > __tmp__ +# - echo set(CMAKE_Fortran_COMPILER "C:/Program\ Files/gcc/bin/gfortran.exe") > CMakeLists.txt +# - echo set(CMAKE_MAKE_PROGRAM "C:/Program Files\ \(x86\)/GnuWin32/bin/make.exe") >> CMakeLists.txt +# - type __tmp__ >> CMakeLists.txt +# - mkdir build +# - cd build +# - cmake -G "MinGW Makefiles" .. +# - make +# artifacts: +# paths: +# - mage/src/build/mage.exe +# - mage/src/build/mage_extraire.exe +# - mage/src/build/mailleurPF.exe build: stage: build @@ -120,8 +120,8 @@ windows-package: artifacts: true - job: build artifacts: true - - job: build-mage-windows - artifacts: true + # - job: build-mage-windows + # artifacts: true rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG artifacts: diff --git a/packages/windows.bat b/packages/windows.bat index 453b0d03..0b276812 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -24,9 +24,9 @@ copy /y ..\VERSION dist\pamhyr\ copy /y ..\AUTHORS dist\pamhyr\ rem MAGE -copy /y ..\mage\src\build\mage.exe dist\pamhyr\ -copy /y ..\mage\src\build\mage_extraire.exe dist\pamhyr\ -copy /y ..\mage\src\build\mailleurPF.exe dist\pamhyr\ +rem copy /y ..\mage\src\build\mage.exe dist\pamhyr\ +rem copy /y ..\mage\src\build\mage_extraire.exe dist\pamhyr\ +rem copy /y ..\mage\src\build\mailleurPF.exe dist\pamhyr\ rem Make installer "C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi From a8c68ba575ae4e5e4920925467dd6f7be686320f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 1 Aug 2023 14:27:56 +0200 Subject: [PATCH 102/289] ci: Minor change. --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2cbfbd42..7c67faec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -163,8 +163,8 @@ tag-release: - name: 'GNU/Linux amd64 (tar.xz)' url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-gnulinux-amd64.tar.xz' filepath: '/packages/pamhyr-gnulinux-amd64.tar.xz' - link_type: 'other' + link_type: 'package' - name: 'Windows amd64 (exe)' url: '${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/packages/pamhyr-win-amd64.exe' filepath: '/packages/pamhyr-win-amd64.exe' - link_type: 'other' + link_type: 'package' From 34eb3e1d53bbebed95d5a91fc6f14f0bee6db8b1 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 1 Aug 2023 16:03:33 +0200 Subject: [PATCH 103/289] LICENSE: Add LICENSE file and files headers. --- .gitlab-ci.yml | 16 + LICENSE | 674 ++++++++++++++++++ packages/linux.sh | 17 + packages/pamhyr.nsi | 7 +- packages/version.sh | 18 +- packages/windows.bat | 18 +- packages/wine.sh | 16 + src/Checker/Checker.py | 16 + src/Checker/Mage.py | 16 + src/Checker/Study.py | 16 + .../BoundaryCondition/BoundaryCondition.py | 16 + .../BoundaryConditionList.py | 16 + .../BoundaryConditionTypes.py | 16 + src/Model/DB.py | 16 + src/Model/Except.py | 16 + src/Model/Friction/Friction.py | 16 + src/Model/Friction/FrictionList.py | 16 + src/Model/Geometry/Point.py | 16 + src/Model/Geometry/PointXY.py | 16 + src/Model/Geometry/PointXYZ.py | 16 + src/Model/Geometry/Profile.py | 16 + src/Model/Geometry/ProfileXYZ.py | 16 + src/Model/Geometry/Reach.py | 16 + src/Model/Geometry/Vector_1d.py | 16 + .../InitialConditions/InitialConditions.py | 16 + .../InitialConditionsDict.py | 16 + .../LateralContribution.py | 16 + .../LateralContributionList.py | 16 + .../LateralContributionTypes.py | 16 + src/Model/Network/Edge.py | 16 + src/Model/Network/Graph.py | 16 + src/Model/Network/Node.py | 16 + src/Model/Network/Point.py | 16 + src/Model/River.py | 16 + src/Model/Saved.py | 16 + src/Model/Serializable.py | 16 + .../SolverParameters/SolverParametersList.py | 16 + src/Model/Stricklers/Stricklers.py | 16 + src/Model/Stricklers/StricklersList.py | 16 + src/Model/Study.py | 16 + src/Scripts/plot_3DST.py | 16 + src/Solver/ASolver.py | 16 + src/Solver/GenericSolver.py | 16 + src/Solver/Mage.py | 16 + src/Solver/Solvers.py | 16 + src/View/ASubWindow.py | 16 + src/View/About/Window.py | 19 +- src/View/BoundaryCondition/Edit/Plot.py | 16 + src/View/BoundaryCondition/Edit/Table.py | 16 + .../BoundaryCondition/Edit/UndoCommand.py | 16 + src/View/BoundaryCondition/Edit/Window.py | 16 + src/View/BoundaryCondition/Edit/translate.py | 16 + src/View/BoundaryCondition/Table.py | 16 + src/View/BoundaryCondition/UndoCommand.py | 16 + src/View/BoundaryCondition/Window.py | 16 + src/View/BoundaryCondition/translate.py | 16 + src/View/CheckList/Table.py | 16 + src/View/CheckList/Window.py | 16 + src/View/CheckList/Worker.py | 16 + src/View/Configure/Solver/Window.py | 16 + src/View/Configure/Window.py | 16 + src/View/Debug/Window.py | 16 + src/View/DummyWindow.py | 16 + src/View/Frictions/PlotStricklers.py | 16 + src/View/Frictions/Table.py | 16 + src/View/Frictions/UndoCommand.py | 16 + src/View/Frictions/Window.py | 16 + src/View/Frictions/translate.py | 16 + src/View/Geometry/PlotAC.py | 16 + src/View/Geometry/PlotKPC.py | 16 + src/View/Geometry/PlotXY.py | 16 + src/View/Geometry/Profile/Plot.py | 16 + src/View/Geometry/Profile/Table.py | 16 + src/View/Geometry/Profile/UndoCommand.py | 16 + src/View/Geometry/Profile/Window.py | 16 + .../Geometry/Profile/mainwindow_ui_profile.py | 16 + src/View/Geometry/Table.py | 16 + src/View/Geometry/UndoCommand.py | 16 + src/View/Geometry/Window.py | 16 + src/View/Geometry/mainwindow_ui_reach.py | 16 + src/View/InitialConditions/DialogDischarge.py | 16 + src/View/InitialConditions/DialogHeight.py | 16 + src/View/InitialConditions/PlotDKP.py | 16 + src/View/InitialConditions/PlotDischarge.py | 16 + src/View/InitialConditions/Table.py | 16 + src/View/InitialConditions/UndoCommand.py | 16 + src/View/InitialConditions/Window.py | 16 + src/View/InitialConditions/translate.py | 16 + src/View/LateralContribution/Edit/Plot.py | 16 + src/View/LateralContribution/Edit/Table.py | 16 + .../LateralContribution/Edit/UndoCommand.py | 16 + src/View/LateralContribution/Edit/Window.py | 16 + .../LateralContribution/Edit/translate.py | 16 + src/View/LateralContribution/Table.py | 16 + src/View/LateralContribution/UndoCommand.py | 16 + src/View/LateralContribution/Window.py | 16 + src/View/LateralContribution/translate.py | 16 + src/View/ListedSubWindow.py | 16 + src/View/MainWindow.py | 16 + src/View/Network/GraphWidget.py | 16 + src/View/Network/Table.py | 16 + src/View/Network/UndoCommand.py | 16 + src/View/Network/Window.py | 16 + src/View/Plot/APlot.py | 16 + src/View/Plot/MplCanvas.py | 16 + src/View/Plot/mpl_canvas_onpick_event.py | 16 + src/View/Plot/navigation_toolbar_2qt.py | 16 + src/View/RunSolver/Log/Window.py | 16 + src/View/RunSolver/Window.py | 16 + src/View/SolverParameters/Table.py | 16 + src/View/SolverParameters/UndoCommand.py | 16 + src/View/SolverParameters/Window.py | 16 + src/View/SolverParameters/translate.py | 16 + src/View/Stricklers/Table.py | 16 + src/View/Stricklers/UndoCommand.py | 16 + src/View/Stricklers/Window.py | 16 + src/View/Stricklers/translate.py | 16 + src/View/Study/Window.py | 16 + src/View/ui/ui_to_py.sh | 16 + src/config.py | 16 + src/lang/constant_string.py | 16 + src/lang/create_ts.sh | 16 + src/pamhyr.py | 25 + src/tools.py | 16 + tools/license.el | 41 ++ 125 files changed, 2687 insertions(+), 4 deletions(-) create mode 100644 LICENSE create mode 100644 tools/license.el diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7c67faec..8fc82ee9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,19 @@ +# .gitlab-ci.yml -- Pamhyr gitlab-ci +# 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 . + stages: - build - test diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..001d0883 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + pamhyr Copyright (C) 2023 INRAE + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/packages/linux.sh b/packages/linux.sh index 899aa4b8..fb48c881 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -1,5 +1,21 @@ #! /bin/sh +# linux.sh -- 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 . + echo " *** RM OLD ENV" rm dist/ -rf @@ -37,6 +53,7 @@ cp -r ../src/lang/*.qm dist/pamhyr/lang/ cp ../VERSION dist/pamhyr/ cp ../AUTHORS dist/pamhyr/ +cp ../LICENSE dist/pamhyr/ cp ../mage/src/build/mage dist/pamhyr/ cp ../mage/src/build/mage_extraire dist/pamhyr/ diff --git a/packages/pamhyr.nsi b/packages/pamhyr.nsi index 35be24ad..ccd25a76 100644 --- a/packages/pamhyr.nsi +++ b/packages/pamhyr.nsi @@ -1,8 +1,12 @@ !include "x64.nsh" -Name "PAMHYR" +!define LIC_NAME "LICENSE" +!define APP_NAME "PAMHYR" +Name "PAMHYR" OutFile "pamhyr-win-amd64.exe" +LicenseData "..\LICENSE" +LicenseText "I Agree" RequestExecutionLevel admin @@ -11,6 +15,7 @@ InstallDir $PROGRAMFILES\PAMHYR InstallDirRegKey HKLM "Software\PAMHYR" "Install_Dir" +Page license Page components Page directory Page instfiles diff --git a/packages/version.sh b/packages/version.sh index aa769b83..8f5e8bcf 100755 --- a/packages/version.sh +++ b/packages/version.sh @@ -1,6 +1,22 @@ #! /bin/sh -# ./version BRANCH TAG COMMIT +# version.sh -- 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 . + +# ./version.sh BRANCH TAG COMMIT if [ -z $2 ]; then diff --git a/packages/windows.bat b/packages/windows.bat index 0b276812..eee8394f 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -1,4 +1,19 @@ -rem Windows batch for pamhyr windows version building +rem windows.bat -- Pamhyr Windows batch for windows version building +rem Copyright (C) 2023 INRAE +rem +rem This program is free software: you can redistribute it and/or modify +rem it under the terms of the GNU General Public License as published by +rem the Free Software Foundation, either version 3 of the License, or +rem (at your option) any later version. +rem +rem This program is distributed in the hope that it will be useful, +rem but WITHOUT ANY WARRANTY; without even the implied warranty of +rem MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +rem GNU General Public License for more details. +rem +rem You should have received a copy of the GNU General Public License +rem along with this program. If not, see . + @ECHO ON rem Python environment @@ -22,6 +37,7 @@ copy /y ..\src\lang\*.qm dist\pamhyr\lang\ rem Information copy /y ..\VERSION dist\pamhyr\ copy /y ..\AUTHORS dist\pamhyr\ +copy /y ..\LICENSE dist\pamhyr\ rem MAGE rem copy /y ..\mage\src\build\mage.exe dist\pamhyr\ diff --git a/packages/wine.sh b/packages/wine.sh index 07c43b6b..4f794c51 100755 --- a/packages/wine.sh +++ b/packages/wine.sh @@ -1,5 +1,21 @@ #! /bin/sh +# wine.sh -- 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 . + echo " *** SETUP ENV" export WINARCH=win64 diff --git a/src/Checker/Checker.py b/src/Checker/Checker.py index 6e87a601..d10a8c4d 100644 --- a/src/Checker/Checker.py +++ b/src/Checker/Checker.py @@ -1,3 +1,19 @@ +# Checker.py -- Pamhyr abstract checker class +# 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 enum import Enum diff --git a/src/Checker/Mage.py b/src/Checker/Mage.py index 4e121d51..edb0c349 100644 --- a/src/Checker/Mage.py +++ b/src/Checker/Mage.py @@ -1,3 +1,19 @@ +# Mage.py -- Pamhyr MAGE checkers +# 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 time diff --git a/src/Checker/Study.py b/src/Checker/Study.py index c6c41fed..868ac104 100644 --- a/src/Checker/Study.py +++ b/src/Checker/Study.py @@ -1,3 +1,19 @@ +# Study.py -- Pamhyr study checkers +# 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 time diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index 152a6269..7df962d2 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -1,3 +1,19 @@ +# BoundaryCondition.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 tools import trace, timer, old_pamhyr_date_to_timestamp diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py index d14a870b..1a342dc7 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionList.py +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -1,3 +1,19 @@ +# BoundaryConditionList.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 copy import copy diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py index c87e5b83..b2c73f11 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -1,3 +1,19 @@ +# BoundaryConditionTypes.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 Model.Except import NotImplementedMethodeError diff --git a/src/Model/DB.py b/src/Model/DB.py index 90869740..ef7d729d 100644 --- a/src/Model/DB.py +++ b/src/Model/DB.py @@ -1,3 +1,19 @@ +# DB.py -- Pamhyr abstract model database classes +# 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 diff --git a/src/Model/Except.py b/src/Model/Except.py index ad311666..7d5e3959 100644 --- a/src/Model/Except.py +++ b/src/Model/Except.py @@ -1,3 +1,19 @@ +# Except.py -- Pamhyr model exceptions +# 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 ( diff --git a/src/Model/Friction/Friction.py b/src/Model/Friction/Friction.py index f7328dd4..18167a86 100644 --- a/src/Model/Friction/Friction.py +++ b/src/Model/Friction/Friction.py @@ -1,3 +1,19 @@ +# Friction.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 tools import trace, timer diff --git a/src/Model/Friction/FrictionList.py b/src/Model/Friction/FrictionList.py index a82c9f22..21e10695 100644 --- a/src/Model/Friction/FrictionList.py +++ b/src/Model/Friction/FrictionList.py @@ -1,3 +1,19 @@ +# FrictionList.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 diff --git a/src/Model/Geometry/Point.py b/src/Model/Geometry/Point.py index 2108d962..b3b51e7d 100644 --- a/src/Model/Geometry/Point.py +++ b/src/Model/Geometry/Point.py @@ -1,3 +1,19 @@ +# Point.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 Model.Except import NotImplementedMethodeError diff --git a/src/Model/Geometry/PointXY.py b/src/Model/Geometry/PointXY.py index 9b84ef7c..7448d045 100644 --- a/src/Model/Geometry/PointXY.py +++ b/src/Model/Geometry/PointXY.py @@ -1,3 +1,19 @@ +# PointXY.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 math import dist diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 0fecd1ea..cffcee97 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -1,3 +1,19 @@ +# PointXYZ.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 math import dist diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index b0328f80..c95d2282 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -1,3 +1,19 @@ +# 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 logging diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index d368b981..9cf15da4 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -1,3 +1,19 @@ +# ProfileXYZ.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 numpy as np diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index b18f2ade..d2a573f1 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -1,3 +1,19 @@ +# 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 logging diff --git a/src/Model/Geometry/Vector_1d.py b/src/Model/Geometry/Vector_1d.py index 51fef7f2..a3da9358 100644 --- a/src/Model/Geometry/Vector_1d.py +++ b/src/Model/Geometry/Vector_1d.py @@ -1,3 +1,19 @@ +# Vector_1d.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 numpy as np diff --git a/src/Model/InitialConditions/InitialConditions.py b/src/Model/InitialConditions/InitialConditions.py index bb106662..3bf747b0 100644 --- a/src/Model/InitialConditions/InitialConditions.py +++ b/src/Model/InitialConditions/InitialConditions.py @@ -1,3 +1,19 @@ +# InitialConditions.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 copy import copy, deepcopy diff --git a/src/Model/InitialConditions/InitialConditionsDict.py b/src/Model/InitialConditions/InitialConditionsDict.py index 5e8d5fba..48da56a0 100644 --- a/src/Model/InitialConditions/InitialConditionsDict.py +++ b/src/Model/InitialConditions/InitialConditionsDict.py @@ -1,3 +1,19 @@ +# InitialConditionsDict.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 copy import copy diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index 5098e9ba..e95733bf 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -1,3 +1,19 @@ +# LateralContribution.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 tools import trace, timer, old_pamhyr_date_to_timestamp diff --git a/src/Model/LateralContribution/LateralContributionList.py b/src/Model/LateralContribution/LateralContributionList.py index 4d6a8567..ca7c111e 100644 --- a/src/Model/LateralContribution/LateralContributionList.py +++ b/src/Model/LateralContribution/LateralContributionList.py @@ -1,3 +1,19 @@ +# LateralContributionList.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 copy import copy diff --git a/src/Model/LateralContribution/LateralContributionTypes.py b/src/Model/LateralContribution/LateralContributionTypes.py index 69d9946c..d669266a 100644 --- a/src/Model/LateralContribution/LateralContributionTypes.py +++ b/src/Model/LateralContribution/LateralContributionTypes.py @@ -1,3 +1,19 @@ +# LateralContributionTypes.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 Model.Except import NotImplementedMethodeError diff --git a/src/Model/Network/Edge.py b/src/Model/Network/Edge.py index f1ced151..12249943 100644 --- a/src/Model/Network/Edge.py +++ b/src/Model/Network/Edge.py @@ -1,3 +1,19 @@ +# Edge.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 Model.Network.Node import Node diff --git a/src/Model/Network/Graph.py b/src/Model/Network/Graph.py index e2d549fb..62948a2b 100644 --- a/src/Model/Network/Graph.py +++ b/src/Model/Network/Graph.py @@ -1,3 +1,19 @@ +# Graph.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 functools import reduce diff --git a/src/Model/Network/Node.py b/src/Model/Network/Node.py index 5f37101f..7b8f52ba 100644 --- a/src/Model/Network/Node.py +++ b/src/Model/Network/Node.py @@ -1,3 +1,19 @@ +# Node.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 Model.Network.Point import Point diff --git a/src/Model/Network/Point.py b/src/Model/Network/Point.py index 8ea0ecd9..0bb12609 100644 --- a/src/Model/Network/Point.py +++ b/src/Model/Network/Point.py @@ -1,3 +1,19 @@ +# Point.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 -*- class Point(object): diff --git a/src/Model/River.py b/src/Model/River.py index a19e6cc3..d0ae5b22 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -1,3 +1,19 @@ +# River.py -- Pamhyr river model +# 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 Model.DB import SQLSubModel diff --git a/src/Model/Saved.py b/src/Model/Saved.py index 3f672731..a1b91fd5 100644 --- a/src/Model/Saved.py +++ b/src/Model/Saved.py @@ -1,3 +1,19 @@ +# Saved.py -- Pamhyr model status class +# 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 diff --git a/src/Model/Serializable.py b/src/Model/Serializable.py index 57f2a4c1..0c083448 100644 --- a/src/Model/Serializable.py +++ b/src/Model/Serializable.py @@ -1,3 +1,19 @@ +# Serializable.py -- Pamhyr pickle abstract class +# 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 pickle diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index 88d1d567..a6471356 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -1,3 +1,19 @@ +# SolverParametersList.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 copy import copy diff --git a/src/Model/Stricklers/Stricklers.py b/src/Model/Stricklers/Stricklers.py index cbc10009..244df20b 100644 --- a/src/Model/Stricklers/Stricklers.py +++ b/src/Model/Stricklers/Stricklers.py @@ -1,3 +1,19 @@ +# Stricklers.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 tools import trace, timer diff --git a/src/Model/Stricklers/StricklersList.py b/src/Model/Stricklers/StricklersList.py index 049d9b83..82076bb1 100644 --- a/src/Model/Stricklers/StricklersList.py +++ b/src/Model/Stricklers/StricklersList.py @@ -1,3 +1,19 @@ +# StricklersList.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 tools import trace, timer diff --git a/src/Model/Study.py b/src/Model/Study.py index bdebce47..af594c90 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -1,3 +1,19 @@ +# Study.py -- Pamhyr Study class +# 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 diff --git a/src/Scripts/plot_3DST.py b/src/Scripts/plot_3DST.py index 2647a32a..bcadf1f1 100644 --- a/src/Scripts/plot_3DST.py +++ b/src/Scripts/plot_3DST.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index e94bac4b..5aaae4ff 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -1,3 +1,19 @@ +# ASolver.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 diff --git a/src/Solver/GenericSolver.py b/src/Solver/GenericSolver.py index a80adf23..9e588bae 100644 --- a/src/Solver/GenericSolver.py +++ b/src/Solver/GenericSolver.py @@ -1,3 +1,19 @@ +# GenericSolver.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 Solver.ASolver import ( diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 125fcfff..0d43d2ef 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -1,3 +1,19 @@ +# Mage.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 diff --git a/src/Solver/Solvers.py b/src/Solver/Solvers.py index 58db6f49..221f1ff4 100644 --- a/src/Solver/Solvers.py +++ b/src/Solver/Solvers.py @@ -1,3 +1,19 @@ +# Solvers.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 diff --git a/src/View/ASubWindow.py b/src/View/ASubWindow.py index 17ae4a07..c07465f1 100644 --- a/src/View/ASubWindow.py +++ b/src/View/ASubWindow.py @@ -1,3 +1,19 @@ +# ASubWindow.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 diff --git a/src/View/About/Window.py b/src/View/About/Window.py index f4a98121..7c269aaf 100644 --- a/src/View/About/Window.py +++ b/src/View/About/Window.py @@ -1,3 +1,19 @@ +# 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 @@ -40,7 +56,8 @@ class AboutWindow(ASubWindow): while True: author = next(f).strip() logger.info(f"author: {author}") - label = f"\n © {author}" + label + label = f"\n - {author}" + label except StopIteration: label = _translate("About", "Contributors: ") + label + label = "Copyright © 2023 INRAE\n" + label self.set_label_text("label_copyright", label) diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index 9f6df875..c9e36413 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -1,3 +1,19 @@ +# Plot.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 datetime import datetime diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index dfe805e1..d479ea46 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- import logging diff --git a/src/View/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py index b2eb09ac..d2b063c9 100644 --- a/src/View/BoundaryCondition/Edit/UndoCommand.py +++ b/src/View/BoundaryCondition/Edit/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 4e8513fa..9ecd240c 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -1,3 +1,19 @@ +# 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 -*- from tools import timer, trace diff --git a/src/View/BoundaryCondition/Edit/translate.py b/src/View/BoundaryCondition/Edit/translate.py index 44ef5a46..4c09f5fc 100644 --- a/src/View/BoundaryCondition/Edit/translate.py +++ b/src/View/BoundaryCondition/Edit/translate.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py index 98890e0c..d1570c05 100644 --- a/src/View/BoundaryCondition/Table.py +++ b/src/View/BoundaryCondition/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from tools import trace, timer diff --git a/src/View/BoundaryCondition/UndoCommand.py b/src/View/BoundaryCondition/UndoCommand.py index 7b1c7f5e..9f8600c4 100644 --- a/src/View/BoundaryCondition/UndoCommand.py +++ b/src/View/BoundaryCondition/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index 2e7dd430..af314574 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/BoundaryCondition/translate.py b/src/View/BoundaryCondition/translate.py index e191d0be..661c1ad0 100644 --- a/src/View/BoundaryCondition/translate.py +++ b/src/View/BoundaryCondition/translate.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/CheckList/Table.py b/src/View/CheckList/Table.py index 071b4907..5810c4be 100644 --- a/src/View/CheckList/Table.py +++ b/src/View/CheckList/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from tools import trace, timer diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index d04c8913..4c40ad1f 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -1,3 +1,19 @@ +# 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 -*- from tools import trace, timer diff --git a/src/View/CheckList/Worker.py b/src/View/CheckList/Worker.py index 9137d26b..efd95ded 100644 --- a/src/View/CheckList/Worker.py +++ b/src/View/CheckList/Worker.py @@ -1,3 +1,19 @@ +# Worker.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 time diff --git a/src/View/Configure/Solver/Window.py b/src/View/Configure/Solver/Window.py index 71d4f798..c295481f 100644 --- a/src/View/Configure/Solver/Window.py +++ b/src/View/Configure/Solver/Window.py @@ -1,3 +1,19 @@ +# 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 -*- from View.ASubWindow import ASubWindow diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index 818e5d9c..23a371ac 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py index 974eace3..346c2ce0 100644 --- a/src/View/Debug/Window.py +++ b/src/View/Debug/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/DummyWindow.py b/src/View/DummyWindow.py index 576b5188..05117bca 100644 --- a/src/View/DummyWindow.py +++ b/src/View/DummyWindow.py @@ -1,3 +1,19 @@ +# DummyWindow.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.ASubWindow import ASubWindow diff --git a/src/View/Frictions/PlotStricklers.py b/src/View/Frictions/PlotStricklers.py index 8f706782..71ff6e6f 100644 --- a/src/View/Frictions/PlotStricklers.py +++ b/src/View/Frictions/PlotStricklers.py @@ -1,3 +1,19 @@ +# PlotStricklers.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 tools import timer, flatten diff --git a/src/View/Frictions/Table.py b/src/View/Frictions/Table.py index 2c96ef97..8a6171da 100644 --- a/src/View/Frictions/Table.py +++ b/src/View/Frictions/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from tools import trace, timer diff --git a/src/View/Frictions/UndoCommand.py b/src/View/Frictions/UndoCommand.py index 0d685ace..911ded8a 100644 --- a/src/View/Frictions/UndoCommand.py +++ b/src/View/Frictions/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 71a3e8e1..d5761485 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/Frictions/translate.py b/src/View/Frictions/translate.py index c404f59c..21981ab1 100644 --- a/src/View/Frictions/translate.py +++ b/src/View/Frictions/translate.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/Geometry/PlotAC.py b/src/View/Geometry/PlotAC.py index 9709d3bc..fe6d933f 100644 --- a/src/View/Geometry/PlotAC.py +++ b/src/View/Geometry/PlotAC.py @@ -1,3 +1,19 @@ +# PlotAC.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- import logging diff --git a/src/View/Geometry/PlotKPC.py b/src/View/Geometry/PlotKPC.py index f3e883f3..7fb31dae 100644 --- a/src/View/Geometry/PlotKPC.py +++ b/src/View/Geometry/PlotKPC.py @@ -1,3 +1,19 @@ +# PlotKPC.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- import logging diff --git a/src/View/Geometry/PlotXY.py b/src/View/Geometry/PlotXY.py index 36de09cc..54e056a9 100644 --- a/src/View/Geometry/PlotXY.py +++ b/src/View/Geometry/PlotXY.py @@ -1,3 +1,19 @@ +# PlotXY.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 tools import timer, trace diff --git a/src/View/Geometry/Profile/Plot.py b/src/View/Geometry/Profile/Plot.py index 2dc7f522..076c2703 100644 --- a/src/View/Geometry/Profile/Plot.py +++ b/src/View/Geometry/Profile/Plot.py @@ -1,3 +1,19 @@ +# Plot.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 diff --git a/src/View/Geometry/Profile/Table.py b/src/View/Geometry/Profile/Table.py index d55ebfcd..3d5f72df 100644 --- a/src/View/Geometry/Profile/Table.py +++ b/src/View/Geometry/Profile/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- import numpy as np diff --git a/src/View/Geometry/Profile/UndoCommand.py b/src/View/Geometry/Profile/UndoCommand.py index d0d45b99..166a004b 100644 --- a/src/View/Geometry/Profile/UndoCommand.py +++ b/src/View/Geometry/Profile/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from tools import trace, timer diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 815cc2ec..cfcf6925 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -1,3 +1,19 @@ +# 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 copy diff --git a/src/View/Geometry/Profile/mainwindow_ui_profile.py b/src/View/Geometry/Profile/mainwindow_ui_profile.py index 8de74df0..0b9443f8 100644 --- a/src/View/Geometry/Profile/mainwindow_ui_profile.py +++ b/src/View/Geometry/Profile/mainwindow_ui_profile.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 8cc29dc8..63ae8e45 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- import time diff --git a/src/View/Geometry/UndoCommand.py b/src/View/Geometry/UndoCommand.py index ed911c66..148f7db3 100644 --- a/src/View/Geometry/UndoCommand.py +++ b/src/View/Geometry/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 8efea1c3..9fd0f6f2 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/Geometry/mainwindow_ui_reach.py b/src/View/Geometry/mainwindow_ui_reach.py index f04ee9e8..747b6ca4 100644 --- a/src/View/Geometry/mainwindow_ui_reach.py +++ b/src/View/Geometry/mainwindow_ui_reach.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/InitialConditions/DialogDischarge.py b/src/View/InitialConditions/DialogDischarge.py index 8d01ef0e..f8e7e32a 100644 --- a/src/View/InitialConditions/DialogDischarge.py +++ b/src/View/InitialConditions/DialogDischarge.py @@ -1,3 +1,19 @@ +# DialogDischarge.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.ASubWindow import ASubWindow diff --git a/src/View/InitialConditions/DialogHeight.py b/src/View/InitialConditions/DialogHeight.py index 91ceadb1..18ad67c8 100644 --- a/src/View/InitialConditions/DialogHeight.py +++ b/src/View/InitialConditions/DialogHeight.py @@ -1,3 +1,19 @@ +# DialogHeight.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.ASubWindow import ASubWindow diff --git a/src/View/InitialConditions/PlotDKP.py b/src/View/InitialConditions/PlotDKP.py index 247605b3..23752aef 100644 --- a/src/View/InitialConditions/PlotDKP.py +++ b/src/View/InitialConditions/PlotDKP.py @@ -1,3 +1,19 @@ +# PlotDKP.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 tools import timer diff --git a/src/View/InitialConditions/PlotDischarge.py b/src/View/InitialConditions/PlotDischarge.py index 07664683..1fe05def 100644 --- a/src/View/InitialConditions/PlotDischarge.py +++ b/src/View/InitialConditions/PlotDischarge.py @@ -1,3 +1,19 @@ +# PlotDischarge.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 tools import timer diff --git a/src/View/InitialConditions/Table.py b/src/View/InitialConditions/Table.py index 13eb5218..ebb9af85 100644 --- a/src/View/InitialConditions/Table.py +++ b/src/View/InitialConditions/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from tools import trace, timer diff --git a/src/View/InitialConditions/UndoCommand.py b/src/View/InitialConditions/UndoCommand.py index 215c299b..9d93f78b 100644 --- a/src/View/InitialConditions/UndoCommand.py +++ b/src/View/InitialConditions/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 35822811..95f6004a 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/InitialConditions/translate.py b/src/View/InitialConditions/translate.py index 43890acb..370d4a22 100644 --- a/src/View/InitialConditions/translate.py +++ b/src/View/InitialConditions/translate.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/LateralContribution/Edit/Plot.py b/src/View/LateralContribution/Edit/Plot.py index d7c95ec0..df088405 100644 --- a/src/View/LateralContribution/Edit/Plot.py +++ b/src/View/LateralContribution/Edit/Plot.py @@ -1,3 +1,19 @@ +# Plot.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 datetime import datetime diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index 4101995c..0fabd5cb 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- import logging diff --git a/src/View/LateralContribution/Edit/UndoCommand.py b/src/View/LateralContribution/Edit/UndoCommand.py index b5963b76..a0a602bc 100644 --- a/src/View/LateralContribution/Edit/UndoCommand.py +++ b/src/View/LateralContribution/Edit/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index e9927710..c3459958 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -1,3 +1,19 @@ +# 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 -*- from tools import timer, trace diff --git a/src/View/LateralContribution/Edit/translate.py b/src/View/LateralContribution/Edit/translate.py index e44147e2..243d9ad8 100644 --- a/src/View/LateralContribution/Edit/translate.py +++ b/src/View/LateralContribution/Edit/translate.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/LateralContribution/Table.py b/src/View/LateralContribution/Table.py index 307fc56e..f5a8bdf1 100644 --- a/src/View/LateralContribution/Table.py +++ b/src/View/LateralContribution/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from tools import trace, timer diff --git a/src/View/LateralContribution/UndoCommand.py b/src/View/LateralContribution/UndoCommand.py index f7e680b3..6b688394 100644 --- a/src/View/LateralContribution/UndoCommand.py +++ b/src/View/LateralContribution/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index b87e9968..003ad48e 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/LateralContribution/translate.py b/src/View/LateralContribution/translate.py index c2b9e789..e9c6deeb 100644 --- a/src/View/LateralContribution/translate.py +++ b/src/View/LateralContribution/translate.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/ListedSubWindow.py b/src/View/ListedSubWindow.py index c88fd7b4..37d64234 100644 --- a/src/View/ListedSubWindow.py +++ b/src/View/ListedSubWindow.py @@ -1,3 +1,19 @@ +# ListedSubWindow.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 diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 10a35a6e..8a94c469 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -1,3 +1,19 @@ +# MainWindow.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 diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index 737ca6ba..aafca9e8 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -1,3 +1,19 @@ +# GraphWidget.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 math diff --git a/src/View/Network/Table.py b/src/View/Network/Table.py index 4dad50f5..1b771779 100644 --- a/src/View/Network/Table.py +++ b/src/View/Network/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from Model.Network.Node import Node diff --git a/src/View/Network/UndoCommand.py b/src/View/Network/UndoCommand.py index d6d38125..c9cce895 100644 --- a/src/View/Network/UndoCommand.py +++ b/src/View/Network/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index 19a5d856..7099cea5 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -1,3 +1,19 @@ +# 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 -*- from PyQt5.QtGui import ( diff --git a/src/View/Plot/APlot.py b/src/View/Plot/APlot.py index cca27087..cd9988cf 100644 --- a/src/View/Plot/APlot.py +++ b/src/View/Plot/APlot.py @@ -1,3 +1,19 @@ +# APlot.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 -*- class APlot(object): diff --git a/src/View/Plot/MplCanvas.py b/src/View/Plot/MplCanvas.py index 5b3fc4a8..ee1f5373 100644 --- a/src/View/Plot/MplCanvas.py +++ b/src/View/Plot/MplCanvas.py @@ -1,3 +1,19 @@ +# MplCanvas.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 . + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure diff --git a/src/View/Plot/mpl_canvas_onpick_event.py b/src/View/Plot/mpl_canvas_onpick_event.py index 010959bc..fdfa1de6 100644 --- a/src/View/Plot/mpl_canvas_onpick_event.py +++ b/src/View/Plot/mpl_canvas_onpick_event.py @@ -1,3 +1,19 @@ +# mpl_canvas_onpick_event.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 . + import logging from time import time diff --git a/src/View/Plot/navigation_toolbar_2qt.py b/src/View/Plot/navigation_toolbar_2qt.py index 37186450..1cca7cf0 100644 --- a/src/View/Plot/navigation_toolbar_2qt.py +++ b/src/View/Plot/navigation_toolbar_2qt.py @@ -1,3 +1,19 @@ +# navigation_toolbar_2qt.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 diff --git a/src/View/RunSolver/Log/Window.py b/src/View/RunSolver/Log/Window.py index fb7ba631..93d42165 100644 --- a/src/View/RunSolver/Log/Window.py +++ b/src/View/RunSolver/Log/Window.py @@ -1,3 +1,19 @@ +# 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 tempfile diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 822f2bb7..dba5a16e 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -1,3 +1,19 @@ +# 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 tempfile diff --git a/src/View/SolverParameters/Table.py b/src/View/SolverParameters/Table.py index 7b44159d..9aa3b55b 100644 --- a/src/View/SolverParameters/Table.py +++ b/src/View/SolverParameters/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from tools import trace, timer diff --git a/src/View/SolverParameters/UndoCommand.py b/src/View/SolverParameters/UndoCommand.py index 6a8dc1eb..3c917e4f 100644 --- a/src/View/SolverParameters/UndoCommand.py +++ b/src/View/SolverParameters/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index 4e154a54..9560251d 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 6085dbfb..76cb70ac 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/Stricklers/Table.py b/src/View/Stricklers/Table.py index 29f8fd53..31dd4d9b 100644 --- a/src/View/Stricklers/Table.py +++ b/src/View/Stricklers/Table.py @@ -1,3 +1,19 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from tools import trace, timer diff --git a/src/View/Stricklers/UndoCommand.py b/src/View/Stricklers/UndoCommand.py index 07e782de..e7dc733e 100644 --- a/src/View/Stricklers/UndoCommand.py +++ b/src/View/Stricklers/UndoCommand.py @@ -1,3 +1,19 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- from copy import deepcopy diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index 1d2bfe43..29f886cb 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/Stricklers/translate.py b/src/View/Stricklers/translate.py index 4664e163..ccafd5d8 100644 --- a/src/View/Stricklers/translate.py +++ b/src/View/Stricklers/translate.py @@ -1,3 +1,19 @@ +# 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 diff --git a/src/View/Study/Window.py b/src/View/Study/Window.py index a90ed549..7218f9d3 100644 --- a/src/View/Study/Window.py +++ b/src/View/Study/Window.py @@ -1,3 +1,19 @@ +# 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 -*- from Model.Study import Study diff --git a/src/View/ui/ui_to_py.sh b/src/View/ui/ui_to_py.sh index 92b12ff2..f4d11a5d 100755 --- a/src/View/ui/ui_to_py.sh +++ b/src/View/ui/ui_to_py.sh @@ -1,5 +1,21 @@ #! /bin/sh +# ui_to_py.sh -- 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 . + for ui in $(ls *.ui) do python3 -m PyQt5.uic.pyuic -x $ui -o ${ui%.*}.py diff --git a/src/config.py b/src/config.py index 46e4ad2e..d18a8e34 100644 --- a/src/config.py +++ b/src/config.py @@ -1,3 +1,19 @@ +# config.py -- Pamhyr configuration manager +# 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 diff --git a/src/lang/constant_string.py b/src/lang/constant_string.py index b599389a..8466a3b4 100644 --- a/src/lang/constant_string.py +++ b/src/lang/constant_string.py @@ -1,3 +1,19 @@ +# constant_string.py -- Pamhyr constant string definition for translate +# 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 diff --git a/src/lang/create_ts.sh b/src/lang/create_ts.sh index c3019916..1b30d684 100755 --- a/src/lang/create_ts.sh +++ b/src/lang/create_ts.sh @@ -1,5 +1,21 @@ #! /bin/sh +# create_ts.sh -- Pamhyr script to generate translate files +# 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 . + SOURCES=$(find ../ -name "*.py") FROM=$(find ../ -name "*.ui") diff --git a/src/pamhyr.py b/src/pamhyr.py index 8a832e18..0f957d70 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -1,4 +1,21 @@ #!/usr/bin/env python3 + +# pamhyr.py -- Pamhyr entrypoint +# 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, os @@ -23,12 +40,20 @@ logging.basicConfig( logger = logging.getLogger() logger.setLevel(logging.INFO) +def license(): + 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: under certain conditions.") + def main(): conf = Config.load() app = QApplication(sys.argv) translator = QTranslator() + license() + lang_file = "" if conf.lang == "": # System language diff --git a/src/tools.py b/src/tools.py index c71c91d1..d5707c56 100644 --- a/src/tools.py +++ b/src/tools.py @@ -1,3 +1,19 @@ +# tools.py -- Pamhyr tools function collection +# 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 diff --git a/tools/license.el b/tools/license.el new file mode 100644 index 00000000..a35e4308 --- /dev/null +++ b/tools/license.el @@ -0,0 +1,41 @@ +;; license.el -- 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 -*- + +(defun pamhyr-current-filename () + (car (last (file-name-split (buffer-file-name))))) + +(defun pamhyr-insert-license () + (interactive) + (let ((filename (pamhyr-current-filename))) + (insert (format + "# %s -- 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 . +" filename)))) From 0f0a08dcbf7b97afbb61b16ebd4397dd11601149 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 1 Aug 2023 17:21:38 +0200 Subject: [PATCH 104/289] packages: Put mage in a sub directory. --- packages/linux.sh | 7 ++++--- packages/windows.bat | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/linux.sh b/packages/linux.sh index fb48c881..9bfddf54 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -55,9 +55,10 @@ cp ../VERSION dist/pamhyr/ cp ../AUTHORS dist/pamhyr/ cp ../LICENSE dist/pamhyr/ -cp ../mage/src/build/mage dist/pamhyr/ -cp ../mage/src/build/mage_extraire dist/pamhyr/ -cp ../mage/src/build/mailleurPF dist/pamhyr/ +mkdir -p dist/pamhyr/MAGE/ +cp ../mage/src/build/mage dist/pamhyr/MAGE/ +cp ../mage/src/build/mage_extraire dist/pamhyr/MAGE/ +cp ../mage/src/build/mailleurPF dist/pamhyr/MAGE/ echo " *** MAKE SRC PACKAGE" diff --git a/packages/windows.bat b/packages/windows.bat index eee8394f..451dc195 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -40,9 +40,10 @@ copy /y ..\AUTHORS dist\pamhyr\ copy /y ..\LICENSE dist\pamhyr\ rem MAGE -rem copy /y ..\mage\src\build\mage.exe dist\pamhyr\ -rem copy /y ..\mage\src\build\mage_extraire.exe dist\pamhyr\ -rem copy /y ..\mage\src\build\mailleurPF.exe dist\pamhyr\ +rem mkdir dist\pamhyr\MAGE +rem copy /y ..\mage\src\build\mage.exe dist\pamhyr\MAGE\ +rem copy /y ..\mage\src\build\mage_extraire.exe dist\pamhyr\MAGE\ +rem copy /y ..\mage\src\build\mailleurPF.exe dist\pamhyr\MAGE\ rem Make installer "C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi From 9c8af7b0a74d3fa97314df3d984ed46c310d5f4a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 1 Aug 2023 17:31:24 +0200 Subject: [PATCH 105/289] Network: Fix 'previous_item' c++ object deleted. --- src/View/Network/GraphWidget.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index aafca9e8..e715b388 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -17,6 +17,7 @@ # -*- coding: utf-8 -*- import math +import logging from tools import timer @@ -39,6 +40,8 @@ from Model.Network.Graph import Graph from View.Network.UndoCommand import * +logger = logging.getLogger() + _translate = QCoreApplication.translate @@ -571,14 +574,17 @@ class GraphWidget(QGraphicsView): Returns: Nothing """ - previous_item = self._selected_item - self._selected_item = item + try: + previous_item = self._selected_item + self._selected_item = item - if previous_item: - previous_item.update() + if previous_item: + previous_item.update() - if item: - item.update() + if item: + item.update() + except Exception as e: + logger.debug(str(e)) def selected_new_edge_src_node(self): """The current node item selected to add new edge From 6500c889d91eec6f71cc9ec7edde82d5609f07bb Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 09:56:01 +0200 Subject: [PATCH 106/289] ci: Downloads mage from release Test5 and add mage to windows versions. --- .gitlab-ci.yml | 60 ++++++++++++++++++++++++++++++++++---------- packages/linux.sh | 6 ++--- packages/windows.bat | 8 +++--- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8fc82ee9..19d0962c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,20 +30,54 @@ build-mage-linux: - linux rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG - variables: - GIT_SUBMODULE_STRATEGY: recursive - GIT_SUBMODULE_DEPTH: 20 script: - - cd mage/src/ - - mkdir -p build - - cd build - - cmake .. - - make + - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/Test5/downloads/packages/mage_linux.tgz + - mkdir -p mage + - cd mage + - tar xvf ../mage.tgz artifacts: paths: - - mage/src/build/mage - - mage/src/build/mage_extraire - - mage/src/build/mailleurPF + - mage/mage + - mage/mage_extraire + - mage/mailleurPF + +# build-mage-linux: +# stage: build +# tags: +# - linux +# rules: +# - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG +# variables: +# GIT_SUBMODULE_STRATEGY: recursive +# GIT_SUBMODULE_DEPTH: 20 +# script: +# - cd mage/src/ +# - mkdir -p build +# - cd build +# - cmake .. +# - make +# artifacts: +# paths: +# - mage/src/build/mage +# - mage/src/build/mage_extraire +# - mage/src/build/mailleurPF + +build-mage-windows: + stage: build + tags: + - linux + rules: + - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG + script: + - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/Test5/downloads/packages/mage_windows.tgz + - mkdir -p mage + - cd mage + - tar xvf ../mage.tgz + artifacts: + paths: + - mage/mage.exe + - mage/mage_extraire.exe + - mage/mailleurPF.exe # build-mage-windows: # stage: build @@ -136,8 +170,8 @@ windows-package: artifacts: true - job: build artifacts: true - # - job: build-mage-windows - # artifacts: true + - job: build-mage-windows + artifacts: true rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG artifacts: diff --git a/packages/linux.sh b/packages/linux.sh index 9bfddf54..023fa079 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -56,9 +56,9 @@ cp ../AUTHORS dist/pamhyr/ cp ../LICENSE dist/pamhyr/ mkdir -p dist/pamhyr/MAGE/ -cp ../mage/src/build/mage dist/pamhyr/MAGE/ -cp ../mage/src/build/mage_extraire dist/pamhyr/MAGE/ -cp ../mage/src/build/mailleurPF dist/pamhyr/MAGE/ +cp ../mage/mage dist/pamhyr/MAGE/ +cp ../mage/mage_extraire dist/pamhyr/MAGE/ +cp ../mage/mailleurPF dist/pamhyr/MAGE/ echo " *** MAKE SRC PACKAGE" diff --git a/packages/windows.bat b/packages/windows.bat index 451dc195..65ac8f61 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -40,10 +40,10 @@ copy /y ..\AUTHORS dist\pamhyr\ copy /y ..\LICENSE dist\pamhyr\ rem MAGE -rem mkdir dist\pamhyr\MAGE -rem copy /y ..\mage\src\build\mage.exe dist\pamhyr\MAGE\ -rem copy /y ..\mage\src\build\mage_extraire.exe dist\pamhyr\MAGE\ -rem copy /y ..\mage\src\build\mailleurPF.exe dist\pamhyr\MAGE\ +mkdir dist\pamhyr\MAGE +copy /y ..\mage\mage.exe dist\pamhyr\MAGE\ +copy /y ..\mage\mage_extraire.exe dist\pamhyr\MAGE\ +copy /y ..\mage\mailleurPF.exe dist\pamhyr\MAGE\ rem Make installer "C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi From c8a6d4ec747d53591b04fe1f0e6acb199c47bd88 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 10:47:44 +0200 Subject: [PATCH 107/289] config: Add default mage version into config. --- src/Solver/ASolver.py | 11 +++++++++++ src/config.py | 24 +++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index 5aaae4ff..bfb47aa7 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -168,12 +168,21 @@ class AbstractSolver(object): # Run # ####### + def _install_dir(self): + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + ".." + ) + ) + def run_input_data_fomater(self): if self._cmd_input == "": self._run_next() return True cmd = self._cmd_input + cmd = cmd.replace("@install_dir", self._install_dir()) cmd = cmd.replace("@path", self._path_input) cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) @@ -196,6 +205,7 @@ class AbstractSolver(object): return True cmd = self._cmd_solver + cmd = cmd.replace("@install_dir", self._install_dir()) cmd = cmd.replace("@path", self._path_solver) cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) @@ -219,6 +229,7 @@ class AbstractSolver(object): return True cmd = self._cmd_output + cmd = cmd.replace("@install_dir", self._install_dir()) cmd = cmd.replace("@path", self._path_output) cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) diff --git a/src/config.py b/src/config.py index d18a8e34..5e98a7af 100644 --- a/src/config.py +++ b/src/config.py @@ -34,7 +34,7 @@ logger = logging.getLogger() class Config(SQL): def __init__(self): - self._version = '0.0.1' + self._version = '0.0.2' self.filename = Config.filename() self.set_default_value() @@ -79,12 +79,34 @@ class Config(SQL): self.commit() + def _update(self): 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}...") + major, minor, release = version.strip().split(".") + + if major == minor == "0": + if int(release) < 2: + # Add default solver + posix = os.name == 'posix' + self.execute(f""" + INSERT INTO solver VALUES ( + 'mage8', + 'default-mage', + 'Default PAMHYR mage 8 version', + + '', '', '', + + '', + '@install_dir/mage/mage{"" if posix else ".exe"} -fp=1 @input', + '' + ) + """) + self.commit() + def _load_solver(self): self._solvers = [] From e5f93e1c851a82f10904e8a26b039cdc810ebec2 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 10:53:32 +0200 Subject: [PATCH 108/289] Trans: Fix french translate shortcut. --- src/lang/fr.ts | 446 ++++++++++++++++++++++++------------------------- 1 file changed, 223 insertions(+), 223 deletions(-) diff --git a/src/lang/fr.ts b/src/lang/fr.ts index 07808a3c..e3581eb9 100644 --- a/src/lang/fr.ts +++ b/src/lang/fr.ts @@ -4,7 +4,7 @@ About - + Contributors: Contributeurs : @@ -12,82 +12,82 @@ BoundaryCondition - + Not associate Non associer - + X - + Y - + Time Temps - + Date Date - + Z (m) Z (m) - + days jours - + day jour - + Not defined Non définie - + Ponctual contribution Contributions ponctuelles - + Time over Z - + Time over Discharge - + Z over Discharge - + Name Nom - + Type Type - + Node Nœud @@ -100,47 +100,47 @@ Checker - + Mage network graph {mode} checker - + Check if the network graph is valid Vérifie si le graph réseau est valide - + Study network reach checker - + Check if exists at least one reach for study Vérifie si il exists au moins un Bief dans l'étude - + Study geometry checker Vérificateur de géometrie de l'étude - + Check if exists geometry for each reach of study Vérifie si la géométrie exists pour chaque bief de l'étude - + Dummy ok - + Dummy warning - + Dummy error @@ -240,7 +240,7 @@ 1000 - + 1000 @@ -361,92 +361,92 @@ Exception - + Generic error message - + Undefined error message - + Method not implemented - + Method - + not implemented - + for class - + Not implemented method - + FileFormatError - + Invalid file format: - + File format error - + Invalid file format - + Invalid file - + format because of - + Clipboard format error - + without header - + with header - + Invalid clipboard data format: - + Clipboard format unknown @@ -502,32 +502,32 @@ Frictions - + Not defined Non définie - + Name Nom - + Begin kp (m) Pk de départ (m) - + End kp (m) Pk de fin (m) - + Begin strickler strickler de départ - + End strickler Strickler de fin @@ -535,27 +535,27 @@ Geometry - + Name Nom - + Kp (m) Pk (m) - + Type Type - + upstream amont - + downstream aval @@ -563,117 +563,117 @@ LateralContribution - + Name Nom - + Minor bed Lit mineur - + Medium bed Lit moyen - + Comment Commentaire - + Not associate Non associer - + X - + Y - + Time Temps - + Date Date - + Z (m) - + days jours - + day jour - + Not defined Non définie - + Lateral contribution Contribution laterale - + Rain Pluie - + Evaporation Évaporation - + Type Type - + Reach Bief - + Begin kp (m) Pk de départ (m) - + End kp (m) Pk de fin (m) - + KP (m) PK (m) - + Elevation (m) Altitude (m) - + Height (m) Hauteur (m) @@ -768,7 +768,7 @@ Ctrl+N - + Ctrl+N @@ -778,7 +778,7 @@ Ctrl+O - + Ctrl+O @@ -813,7 +813,7 @@ Ctrl+S - + Ctrl+S @@ -823,7 +823,7 @@ Ctrl+Shift+S - + Ctrl+Shift+S @@ -843,7 +843,7 @@ Ctrl+F4 - + Ctrl+F4 @@ -958,7 +958,7 @@ F5 - + F5 @@ -1023,7 +1023,7 @@ Ctrl+F - + Ctrl+F @@ -1033,7 +1033,7 @@ Ctrl+Q - + Ctrl+Q @@ -1048,7 +1048,7 @@ Ctrl+C - + Ctrl+C @@ -1178,7 +1178,7 @@ Ctrl+Return - + Ctrl+Return @@ -1218,7 +1218,7 @@ Ctrl+D - + Ctrl+D @@ -1233,7 +1233,7 @@ Ctrl+E - + Ctrl+E @@ -1363,7 +1363,7 @@ Quit the application (Ctrl+Q) - + Quitter l'application (Ctrl+Q) @@ -1409,159 +1409,159 @@ MainWindowProfile - + Profile - + (no name) - + Quittez ? - + MainWindowProfile - + Trier les points par ordre croissant de X - + Trier les points par ordre croissant de Y - + Nom - + Abs en travers (m) - + La cote du fond Z minimale - + La cote maximale Z maximale - + Rive gauche - + Rive droite - + Abscisse en travers (m) - + Cote (m) - + Suppression les lignes incomplètes - + Supprimer les lignes des cellules non renseignées ? - + Suppression des noms répétés - + Etes-vous sûr de vouloir quitter ? - + Insérer un point - + Supprimer le/les point(s) sélectionnés - + Trier les points par ordre décroissant de X - + Trier les points par ordre décroissant de Y - + Décaler le point sélectionné vers le haut - + Décaler le point sélectionné vers le bas - + Exporter (dans un fichier) les points du profil au format tabulé - + Copier la sélection au format tabulé - + Coller la sélection depuis le presse-papier au format tabulé - + Vérifier la validité de la saisie et garder ou pas les modifications apportées - + Annuler toutes les modifications depuis la dernière validation - + Annuler toutes les modifications et revenir à l'état initial - + Ligne d'eau : Z : Cote (m) A : Aire mouillée (mu00B2) @@ -1570,12 +1570,12 @@ - + 'Maj + Clic' : Ligne d'eau & 'Ctrl + Clic' : Sélectionner des points - + Abscisse en travers calculée en projétant les points sur le plan défini par les deux points nommés extrêmes @@ -1584,249 +1584,249 @@ sur le plan défini par les deux points nommés extrêmes MainWindow_reach - + Stricklers - + Kp (m) Pk (m) - + Ouvrir un fichier - + Fichiers .ST (*.ST) - + Fichiers .M (*.M) - + Tous les fichiers (*) - + Kp : - + Files .ST(*.ST or *.st) - + All files (*) - + Abscisse en travers (m) - + Cote (m) - + Profil suivant - + Jeu de sections du Bief - + Ordre des sections : Amont --> Aval - + Pk = - + Nouveau profil - + Trier les profils par ordre croissant des Pk - + Name Nom - + Type Type - + Alt+Z - + Alt+E - + Alt+R - + Vue globale automatique (Alt+S) - + Vue globale automatique (Alt+D) - + Vue globale automatique (Alt+F) - + X (m) - + Y (m) - + Draft (m) - + KP (m) - + Discharge (m^3/s) Débit (m³/s) - + Choisissez un nom de fichier à sauvegarder - + Édition des profils sélectionnés - + Vous avez sélectionné plus de 5 profils. Seuls les 5 premiers seront édités. - + Profil N° : - + Profil précédent - + Profil sélectionné - + Importer une géométrie - + Supprimer le profil sélectionné - + Éditer le profil sélectionné - + Copier le profil sélectionné - + Coller le profil en fin de liste (penser à modifier le Pk avant de trier) - + Dupliquer la section sélectionnée - + Trier les profils par ordre décroissant des Pk - + Changer l'ordre des profils (en décalant le profil sélectionné vers le haut) - + Changer l'ordre des profils (en décalant le profil sélectionné vers le bas) - + Terminer l'édition - + Vue isométrique (Alt+Z) - + Vue isométrique (Alt+E) - + Vue isométrique (Alt+R) @@ -1834,37 +1834,37 @@ Seuls les 5 premiers seront édités. Network - + Add node Ajouter un nœud - + Delete the node Supprimer un nœud - + Disable the node Déactiver un nœud - + Delete the reach Supprimer un bief - + Disable the reach Déactiver un bief - + Enable the reach Activer un bief - + Reverse the reach orientation Inverser l'orientation du bief @@ -1872,162 +1872,162 @@ Seuls les 5 premiers seront édités. SolverParameters - + Name Nom - + Value Valeur - + Yes Oui - + No Non - + Y O - + N N - + Initial time (jj:hh:mm:ss) Temps initial (jj:hh:mm:ss) - + Final time (jj:hh:mm:ss) Temps final (jj:hh:mm:ss) - + Timestep (second) Pas de temps (en second) - + Minimum timestep (second) Pas de temps minimal (en second) - + Time step of writing on .TRA Pas de temps d'écriture dans le fichier .TRA - + Time step of writing on .BIN Pas de temps d'écriture dans le fichier .BIN - + Implicitation parameter - + Continuity discretization type (S/L) - + QSJ discretization (A/B) - + Stop criterion iterations (G/A/R) - + Iteration type - + Smoothing coefficient - + Maximun accepted number of CFL - + Minimum water height (meter) - + Maximun number of iterations (< 100) - + Timestep reduction factor - + Reduction precision factor of Z - + Reduction precision factor of Q - + Reduction precision factor of residue - + Number of iteration at maximum precision - + Number of iteration before switch - + Maximum accepted Froude number - + Diffluence node height balance - + Compute reach volume balance (Y/N) - + Maximum reach volume balance - + Minimum reach volume to check @@ -2035,12 +2035,12 @@ Seuls les 5 premiers seront édités. Solvers - + Generic Générique - + Mage version 8 Mage en version 8 @@ -2048,42 +2048,42 @@ Seuls les 5 premiers seront édités. Toolbar - + Vue originale - + Panoramique des axes avec la souris gauche, zoom avec la droite - + Zoom - + Vue globale automatique (Shift+X) - + Enregistrer la figure - + Retour à la vue précédente - + Passer à la vue suivante - + Vue isométrique (Shift+W) From 4f0f2514de30142068069cd6493a2c1f949e245f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 11:01:50 +0200 Subject: [PATCH 109/289] Config: Add default mage solver for new configuration. --- src/config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/config.py b/src/config.py index 5e98a7af..16ec6b12 100644 --- a/src/config.py +++ b/src/config.py @@ -250,6 +250,13 @@ class Config(SQL): # Solvers self._solvers = [] + posix = os.name == 'posix' + ctor = solver_type_list["mage8"] + new = ctor("default-mage") + new._description = "Default PAMHYR mage 8 version" + new._cmd_solver = f"""@install_dir/mage/mage{"" if posix else ".exe"} -fp=1 @input""" + self._solvers.append(new) + # Meshing tool self.meshing_tool = "" From 20329eba229f2d7536fd4b9671951ee569bfb694 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 11:25:52 +0200 Subject: [PATCH 110/289] Packages: Add test case Saar to package. --- packages/linux.sh | 4 ++++ packages/windows.bat | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/packages/linux.sh b/packages/linux.sh index 023fa079..83f5c6fe 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -60,6 +60,10 @@ cp ../mage/mage dist/pamhyr/MAGE/ cp ../mage/mage_extraire dist/pamhyr/MAGE/ cp ../mage/mailleurPF dist/pamhyr/MAGE/ +mkdir -p dist/pamhyr/tests_cases/ +mkdir -p dist/pamhyr/tests_cases/Saar +cp ../tests_cases/Saar/Saar.pamhyr dist/pamhyr/tests_cases/Saar/ + echo " *** MAKE SRC PACKAGE" OLD_PWD=$PWD diff --git a/packages/windows.bat b/packages/windows.bat index 65ac8f61..d8f6e2a5 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -45,5 +45,11 @@ copy /y ..\mage\mage.exe dist\pamhyr\MAGE\ copy /y ..\mage\mage_extraire.exe dist\pamhyr\MAGE\ copy /y ..\mage\mailleurPF.exe dist\pamhyr\MAGE\ +rem Copy tests_cases +mkdir dist\pamhyr\tests_cases +mkdir dist\pamhyr\tests_cases\Saar +copy /y ..\tests_cases\Saar\Saar.pamhyr dist\pamhyr\tests_cases\Saar\ + + rem Make installer "C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi From 17d4b157610a4f44761d5137b9e41c9cdcf68476 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 13:40:46 +0200 Subject: [PATCH 111/289] SedimentLayer: Add d50, sigma and critical_constraint data. --- src/Model/SedimentLayer/SedimentLayer.py | 45 +++++++++++++++++++-- src/View/SedimentLayers/Edit/Table.py | 24 +++++++++++ src/View/SedimentLayers/Edit/UndoCommand.py | 45 +++++++++++++++++++++ src/View/SedimentLayers/Edit/translate.py | 3 ++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py index 1bbcb602..638212c8 100644 --- a/src/Model/SedimentLayer/SedimentLayer.py +++ b/src/Model/SedimentLayer/SedimentLayer.py @@ -11,7 +11,9 @@ class Layer(SQLSubModel): def __init__(self, id:int = -1, name:str = "", - type = "", height = 0, + type = "", + height = 0.0, d50 = 0.0, sigma = 0.0, + critical_constraint = 0.0, sl = None, status = None): super(Layer, self).__init__() @@ -19,7 +21,11 @@ class Layer(SQLSubModel): self._name = name self._type = type + self._height = height + self._d50 = d50 + self._sigma = sigma + self._critical_constraint = critical_constraint if id == -1: self.id = Layer._id_cnt @@ -52,6 +58,30 @@ class Layer(SQLSubModel): def height(self, height): self._height = float(height) + @property + def d50(self): + return self._d50 + + @d50.setter + def d50(self, d50): + self._d50 = float(d50) + + @property + def sigma(self): + return self._sigma + + @sigma.setter + def sigma(self, sigma): + self._sigma = float(sigma) + + @property + def critical_constraint(self): + return self._critical_constraint + + @critical_constraint.setter + def critical_constraint(self, critical_constraint): + self._critical_constraint = float(critical_constraint) + @classmethod def _sql_create(cls, execute): execute(""" @@ -61,6 +91,9 @@ class Layer(SQLSubModel): name TEXT NOT NULL, type TEXT NOT NULL, height REAL NOT NULL, + d50 REAL NOT NULL, + sigma REAL NOT NULL, + critical_constraint REAL NOT NULL, sl INTEGER, FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) ) @@ -83,7 +116,7 @@ class Layer(SQLSubModel): sl = data["sl"] table = execute( - "SELECT id, ind, name, type, height " + + "SELECT id, ind, name, type, height, d50, sigma, critical_constraint " + "FROM sedimentary_layer_layer " + f"WHERE sl = {sl}" ) @@ -97,6 +130,8 @@ class Layer(SQLSubModel): 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'] ) @@ -110,10 +145,12 @@ class Layer(SQLSubModel): sql = ( "INSERT INTO " + - "sedimentary_layer_layer(id, ind, name, type, height, 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}', {sl.id}" + + f"'{self._sql_format(self._type)}', {self._height}, " + + f"{self._d50}, {self._sigma}, {self._critical_constraint}, " + + f"{sl.id}" + ")" ) execute(sql) diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index 91a13b3f..e1a41cb2 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -54,6 +54,12 @@ class TableModel(QAbstractTableModel): return self._sl.get(row).type elif self._headers[column] == "height": return self._sl.get(row).height + elif self._headers[column] == "d50": + return self._sl.get(row).d50 + elif self._headers[column] == "sigma": + return self._sl.get(row).sigma + elif self._headers[column] == "critical_constraint": + return self._sl.get(row).critical_constraint return QVariant() @@ -88,6 +94,24 @@ class TableModel(QAbstractTableModel): self._sl, row, value ) ) + if self._headers[column] == "d50": + self._undo.push( + SetD50Command( + self._sl, row, value + ) + ) + if self._headers[column] == "sigma": + self._undo.push( + SetSigmaCommand( + self._sl, row, value + ) + ) + if self._headers[column] == "critical_constraint": + self._undo.push( + SetCriticalConstraintCommand( + self._sl, row, value + ) + ) self.dataChanged.emit(index, index) return True diff --git a/src/View/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py index e6964709..298fa529 100644 --- a/src/View/SedimentLayers/Edit/UndoCommand.py +++ b/src/View/SedimentLayers/Edit/UndoCommand.py @@ -55,6 +55,51 @@ 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) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).d50 + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).d50 = self._old + + 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) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).sigma + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).sigma = self._old + + 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) + + self._sediment_layers = sediment_layers + self._index = index + self._old = self._sediment_layers.get(self._index).critical_constraint + self._new = new_value + + def undo(self): + self._sediment_layers.get(self._index).critical_constraint = self._old + + 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) diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py index 3b61d18c..fe9e44a9 100644 --- a/src/View/SedimentLayers/Edit/translate.py +++ b/src/View/SedimentLayers/Edit/translate.py @@ -8,4 +8,7 @@ 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"), } From 47ec0ac9b50683098ea1ff1043daf18fc1518473 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 14:06:35 +0200 Subject: [PATCH 112/289] SL: Plots reverse draw order. --- src/View/SedimentLayers/Edit/Plot.py | 10 +++++----- src/View/SedimentLayers/Reach/Plot.py | 6 +++--- src/View/SedimentLayers/Reach/Profile/Plot.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index 14ebb948..8f25c436 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -73,16 +73,16 @@ class Plot(APlot): [z] ) - for i, zsl in enumerate(z_sl): + for i, zsl in enumerate(reversed(z_sl)): self.line_kp_sl.append(None) self.line_kp_sl[i], = self.canvas.axes.plot( x, zsl, - label=names[i], - linestyle="solid" if i == 0 else "--", + label=names[-(i+1)], + linestyle="solid" if i == len(names) - 1 else "--", lw=1.8, - color='grey' if i == 0 else None + color='grey' if i == len(names) - 1 else None ) - self.canvas.axes.text(x[0] + 0.01, zsl[0] + 0.01, f'{names[i]}') + 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/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py index 1ab6daba..a312a8ce 100644 --- a/src/View/SedimentLayers/Reach/Plot.py +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -69,13 +69,13 @@ class Plot(APlot): [z_min] ) - for i, z in enumerate(z_sl): + for i, z in enumerate(reversed(z_sl)): self.line_kp_sl.append(None) self.line_kp_sl[i], = self.canvas.axes.plot( kp, z, - linestyle="solid" if i == 0 else "--", + linestyle="solid" if i == len(z_sl) - 1 else "--", lw=1.8, - color='grey' if i == 0 else None + color='grey' if i == len(z_sl) - 1 else None ) self.canvas.figure.tight_layout() diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py index b8c9b66f..50b4bba2 100644 --- a/src/View/SedimentLayers/Reach/Profile/Plot.py +++ b/src/View/SedimentLayers/Reach/Profile/Plot.py @@ -68,13 +68,13 @@ class Plot(APlot): [z] ) - for i, zsl in enumerate(z_sl): + for i, zsl in enumerate(reversed(z_sl)): self.line_kp_sl.append(None) self.line_kp_sl[i], = self.canvas.axes.plot( x, zsl, - linestyle="solid" if i == 0 else "--", + linestyle="solid" if i == len(z_sl) - 1 else "--", lw=1.8, - color='grey' if i == 0 else None + color='grey' if i == len(z_sl) - 1 else None ) self.canvas.figure.tight_layout() From e199ac7d5ef4defd1a86bf12ae0d80776919e299 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 15:02:43 +0200 Subject: [PATCH 113/289] SL: SL edit disable X axis. --- src/View/SedimentLayers/Edit/Plot.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index 8f25c436..e48ec5eb 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -34,18 +34,19 @@ class Plot(APlot): self.canvas.axes.cla() self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) - if self.data is None: - return - - self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "X (m)"), - color='green', fontsize=12 - ) + # 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)"), color='green', fontsize=12 ) + if self.data is None: + return + x = [0,1] z = [0,0] sl = self.data.height() From ee22dab89c22f2d57b708f746e0d7039cc6b7451 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 15:07:13 +0200 Subject: [PATCH 114/289] Packages: Fix mage repository name. --- packages/linux.sh | 8 ++++---- packages/windows.bat | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/linux.sh b/packages/linux.sh index 83f5c6fe..44770b6e 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -55,10 +55,10 @@ cp ../VERSION dist/pamhyr/ cp ../AUTHORS dist/pamhyr/ cp ../LICENSE dist/pamhyr/ -mkdir -p dist/pamhyr/MAGE/ -cp ../mage/mage dist/pamhyr/MAGE/ -cp ../mage/mage_extraire dist/pamhyr/MAGE/ -cp ../mage/mailleurPF dist/pamhyr/MAGE/ +mkdir -p dist/pamhyr/mage/ +cp ../mage/mage dist/pamhyr/mage/ +cp ../mage/mage_extraire dist/pamhyr/mage/ +cp ../mage/mailleurPF dist/pamhyr/mage/ mkdir -p dist/pamhyr/tests_cases/ mkdir -p dist/pamhyr/tests_cases/Saar diff --git a/packages/windows.bat b/packages/windows.bat index d8f6e2a5..16aba85c 100644 --- a/packages/windows.bat +++ b/packages/windows.bat @@ -40,16 +40,15 @@ copy /y ..\AUTHORS dist\pamhyr\ copy /y ..\LICENSE dist\pamhyr\ rem MAGE -mkdir dist\pamhyr\MAGE -copy /y ..\mage\mage.exe dist\pamhyr\MAGE\ -copy /y ..\mage\mage_extraire.exe dist\pamhyr\MAGE\ -copy /y ..\mage\mailleurPF.exe dist\pamhyr\MAGE\ +mkdir dist\pamhyr\mage +copy /y ..\mage\mage.exe dist\pamhyr\mage\ +copy /y ..\mage\mage_extraire.exe dist\pamhyr\mage\ +copy /y ..\mage\mailleurPF.exe dist\pamhyr\mage\ rem Copy tests_cases mkdir dist\pamhyr\tests_cases mkdir dist\pamhyr\tests_cases\Saar copy /y ..\tests_cases\Saar\Saar.pamhyr dist\pamhyr\tests_cases\Saar\ - rem Make installer "C:\Program Files (x86)\NSIS\makensis.exe" pamhyr.nsi From 304f7f1495b4fbbe1a10671f369a5a1d205c4808 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 2 Aug 2023 15:12:13 +0200 Subject: [PATCH 115/289] Network, BC: Fix mouseReleaseEvent and contextMenuEvent Exception. --- src/View/Network/GraphWidget.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index e715b388..870792b8 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -736,6 +736,9 @@ class GraphWidget(QGraphicsView): def mouseReleaseEvent(self, event): self.clicked = False + if self._only_display: + return + if self._state == "move": if self._current_moved_node is not None: pos = self.mapToScene(event.pos()) @@ -841,6 +844,9 @@ class GraphWidget(QGraphicsView): self.reverse_edge(items[0]) def contextMenuEvent(self, event): + if self._only_display: + return + pos = self.mapToScene(event.pos()) items = self.items(event.pos()) From e8ae1c06316fd10b8f03cc6f377712afb37dfb26 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 09:49:34 +0200 Subject: [PATCH 116/289] Stricklers: Fix minor and medium bed coefficient type. --- src/Model/Stricklers/Stricklers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/Stricklers/Stricklers.py b/src/Model/Stricklers/Stricklers.py index 244df20b..baa484fa 100644 --- a/src/Model/Stricklers/Stricklers.py +++ b/src/Model/Stricklers/Stricklers.py @@ -140,7 +140,7 @@ class Stricklers(SQLSubModel): @minor.setter def minor(self, minor): - self._minor = int(minor) + self._minor = float(minor) @property def medium(self): @@ -148,4 +148,4 @@ class Stricklers(SQLSubModel): @medium.setter def medium(self, medium): - self._medium = int(medium) + self._medium = float(medium) From 464209b6e5b81bc57b6d1b5ed22f5668652a5e46 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 10:40:32 +0200 Subject: [PATCH 117/289] pamhyr: Catch some table data type exception (crash Fix). --- src/View/BoundaryCondition/Edit/Table.py | 13 ++-- .../BoundaryCondition/Edit/UndoCommand.py | 3 +- src/View/BoundaryCondition/Table.py | 41 +++++++----- src/View/BoundaryCondition/UndoCommand.py | 2 +- src/View/Frictions/Table.py | 59 +++++++++-------- src/View/Frictions/UndoCommand.py | 6 +- src/View/Geometry/Profile/Table.py | 64 +++++++++++-------- src/View/Geometry/Profile/UndoCommand.py | 18 +++++- src/View/Geometry/Table.py | 33 ++++++---- src/View/Geometry/UndoCommand.py | 10 ++- src/View/InitialConditions/Table.py | 18 ++++-- src/View/InitialConditions/UndoCommand.py | 7 +- src/View/LateralContribution/Edit/Table.py | 13 ++-- .../LateralContribution/Edit/UndoCommand.py | 3 +- src/View/LateralContribution/Table.py | 61 ++++++++++-------- src/View/LateralContribution/UndoCommand.py | 6 +- src/View/Network/Table.py | 53 ++++++++------- src/View/SolverParameters/Table.py | 23 ++++--- src/View/SolverParameters/UndoCommand.py | 2 +- src/View/Stricklers/Table.py | 50 +++++++++------ src/View/Stricklers/UndoCommand.py | 8 +-- 21 files changed, 303 insertions(+), 190 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index d479ea46..6536bd0d 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -17,6 +17,7 @@ # -*- coding: utf-8 -*- import logging +import traceback from datetime import date, time, datetime, timedelta @@ -212,11 +213,15 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - self._undo.push( - SetDataCommand( - self._data, row, column, value + try: + self._undo.push( + SetDataCommand( + self._data, row, column, value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py index d2b063c9..4bb2605f 100644 --- a/src/View/BoundaryCondition/Edit/UndoCommand.py +++ b/src/View/BoundaryCondition/Edit/UndoCommand.py @@ -33,7 +33,8 @@ class SetDataCommand(QUndoCommand): self._index = index self._column = column self._old = self._data.get_i(self._index)[self._column] - self._new = new_value + _type = self._data.get_type_column(self._column) + self._new = _type(new_value) def undo(self): self._data._set_i_c_v(self._index, self._column, self._old) diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py index d1570c05..fd39a13a 100644 --- a/src/View/BoundaryCondition/Table.py +++ b/src/View/BoundaryCondition/Table.py @@ -16,6 +16,9 @@ # -*- coding: utf-8 -*- +import logging +import traceback + from tools import trace, timer from PyQt5.QtCore import ( @@ -43,6 +46,8 @@ from View.BoundaryCondition.UndoCommand import ( ) from View.BoundaryCondition.translate import * +logger = logging.getLogger() + _translate = QCoreApplication.translate class ComboBoxDelegate(QItemDelegate): @@ -153,25 +158,29 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._headers[column] == "name": - self._undo.push( - SetNameCommand( - self._bcs, self._tab,row, value + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._bcs, self._tab,row, value + ) ) - ) - elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) - self._undo.push( - SetTypeCommand( - self._bcs, self._tab,row, BC_types[key] + elif self._headers[column] == "type": + key = next(k for k, v in long_types.items() if v == value) + self._undo.push( + SetTypeCommand( + self._bcs, self._tab,row, BC_types[key] + ) ) - ) - elif self._headers[column] == "node": - self._undo.push( - SetNodeCommand( - self._bcs, self._tab,row, self._data.node(value) + elif self._headers[column] == "node": + self._undo.push( + SetNodeCommand( + self._bcs, self._tab,row, self._data.node(value) + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/BoundaryCondition/UndoCommand.py b/src/View/BoundaryCondition/UndoCommand.py index 9f8600c4..bbf8cb2c 100644 --- a/src/View/BoundaryCondition/UndoCommand.py +++ b/src/View/BoundaryCondition/UndoCommand.py @@ -34,7 +34,7 @@ class SetNameCommand(QUndoCommand): self._tab = tab self._index = index self._old = self._bcs.get(self._tab, self._index).name - self._new = new_value + self._new = str(new_value) def undo(self): self._bcs.get(self._tab, self._index).name = self._old diff --git a/src/View/Frictions/Table.py b/src/View/Frictions/Table.py index 8a6171da..ef9dc32f 100644 --- a/src/View/Frictions/Table.py +++ b/src/View/Frictions/Table.py @@ -16,6 +16,9 @@ # -*- coding: utf-8 -*- +import logging +import traceback + from tools import trace, timer from PyQt5.QtCore import ( @@ -40,6 +43,8 @@ from View.Frictions.UndoCommand import ( from View.Frictions.translate import * +logger = logging.getLogger() + _translate = QCoreApplication.translate class ComboBoxDelegate(QItemDelegate): @@ -148,36 +153,40 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._headers[column] == "name": - self._undo.push( - SetNameCommand( - self._frictions, row, value + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._frictions, row, value + ) ) - ) - elif self._headers[column] == "begin_kp": - self._undo.push( - SetBeginCommand( - self._frictions, row, value + elif self._headers[column] == "begin_kp": + self._undo.push( + SetBeginCommand( + self._frictions, row, value + ) ) - ) - elif self._headers[column] == "end_kp": - self._undo.push( - SetEndCommand( - self._frictions, row, value + elif self._headers[column] == "end_kp": + self._undo.push( + SetEndCommand( + self._frictions, row, value + ) ) - ) - elif self._headers[column] == "begin_strickler": - self._undo.push( - SetBeginStricklerCommand( - self._frictions, row, self._study.river.strickler(value) + elif self._headers[column] == "begin_strickler": + self._undo.push( + SetBeginStricklerCommand( + self._frictions, row, self._study.river.strickler(value) + ) ) - ) - elif self._headers[column] == "end_strickler": - self._undo.push( - SetEndStricklerCommand( - self._frictions, row, self._study.river.strickler(value) + elif self._headers[column] == "end_strickler": + self._undo.push( + SetEndStricklerCommand( + self._frictions, row, self._study.river.strickler(value) + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/Frictions/UndoCommand.py b/src/View/Frictions/UndoCommand.py index 911ded8a..ef902f07 100644 --- a/src/View/Frictions/UndoCommand.py +++ b/src/View/Frictions/UndoCommand.py @@ -33,7 +33,7 @@ class SetNameCommand(QUndoCommand): self._frictions = frictions self._index = index self._old = self._frictions.get(self._index).name - self._new = new_value + self._new = str(new_value) def undo(self): self._frictions.get(self._index).name = self._old @@ -48,7 +48,7 @@ class SetBeginCommand(QUndoCommand): self._frictions = frictions self._index = index self._old = self._frictions.get(self._index).begin_kp - self._new = new_value + self._new = float(new_value) def undo(self): self._frictions.get(self._index).begin_kp = float(self._old) @@ -63,7 +63,7 @@ class SetEndCommand(QUndoCommand): self._frictions = frictions self._index = index self._old = self._frictions.get(self._index).end_kp - self._new = new_value + self._new = float(new_value) def undo(self): self._frictions.get(self._index).end_kp = float(self._old) diff --git a/src/View/Geometry/Profile/Table.py b/src/View/Geometry/Profile/Table.py index 3d5f72df..2de665aa 100644 --- a/src/View/Geometry/Profile/Table.py +++ b/src/View/Geometry/Profile/Table.py @@ -17,6 +17,8 @@ # -*- coding: utf-8 -*- import numpy as np +import logging +import traceback from tools import timer, trace @@ -36,6 +38,8 @@ from Model.Geometry.ProfileXYZ import ProfileXYZ from View.Geometry.Profile.UndoCommand import * +logger = logging.getLogger() + _translate = QCoreApplication.translate @@ -148,38 +152,42 @@ class TableEditableModel(QAbstractTableModel): column = index.column() if role == Qt.EditRole: - if column == 0: - self._undo_stack.push( - SetXCommand( - self._profile, row, - self._profile.point(row).x, - value + try: + if column == 0: + self._undo_stack.push( + SetXCommand( + self._profile, row, + self._profile.point(row).x, + value + ) ) - ) - elif column == 1: - self._undo_stack.push( - SetYCommand( - self._profile, row, - self._profile.point(row).y, - value + elif column == 1: + self._undo_stack.push( + SetYCommand( + self._profile, row, + self._profile.point(row).y, + value + ) ) - ) - elif column == 2: - self._undo_stack.push( - SetZCommand( - self._profile, row, - self._profile.point(row).z, - value + elif column == 2: + self._undo_stack.push( + SetZCommand( + self._profile, row, + self._profile.point(row).z, + value + ) ) - ) - elif column == 3: - self._undo_stack.push( - SetNameCommand( - self._profile, row, - self._profile.point(row).name, - value + elif column == 3: + self._undo_stack.push( + SetNameCommand( + self._profile, row, + self._profile.point(row).name, + value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/Geometry/Profile/UndoCommand.py b/src/View/Geometry/Profile/UndoCommand.py index 166a004b..7f0cd138 100644 --- a/src/View/Geometry/Profile/UndoCommand.py +++ b/src/View/Geometry/Profile/UndoCommand.py @@ -32,9 +32,13 @@ class SetDataCommand(QUndoCommand): self._profile = profile self._index = index self._old = old_value - self._new = new_value + self._new = self.type(new_value) class SetXCommand(SetDataCommand): + def __init__(self, reach, index, old_value, new_value): + self.type = float + super(SetXCommand, self).__init__(reach, index, old_value, new_value) + def undo(self): self._profile.point(self._index).x = self._old @@ -42,6 +46,10 @@ class SetXCommand(SetDataCommand): self._profile.point(self._index).x = self._new class SetYCommand(SetDataCommand): + def __init__(self, reach, index, old_value, new_value): + self.type = float + super(SetYCommand, self).__init__(reach, index, old_value, new_value) + def undo(self): self._profile.point(self._index).y = self._old @@ -49,6 +57,10 @@ class SetYCommand(SetDataCommand): self._profile.point(self._index).y = self._new class SetZCommand(SetDataCommand): + def __init__(self, reach, index, old_value, new_value): + self.type = float + super(SetZCommand, self).__init__(reach, index, old_value, new_value) + def undo(self): self._profile.point(self._index).z = self._old @@ -56,6 +68,10 @@ class SetZCommand(SetDataCommand): 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) + def undo(self): self._profile.point(self._index).name = self._old diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 63ae8e45..1b055209 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -17,6 +17,8 @@ # -*- coding: utf-8 -*- import time +import logging +import traceback from tools import timer, trace @@ -37,6 +39,7 @@ from Model.Geometry import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ from View.Geometry.UndoCommand import * +logger = logging.getLogger() _translate = QCoreApplication.translate @@ -114,23 +117,27 @@ class TableEditableModel(QAbstractTableModel): column = index.column() if role == Qt.EditRole and index.column() != 2: - if index.column() == 0: - self._undo_stack.push( - SetNameCommand( - self._reach, index.row(), - self._reach.profile(index.row()).name, - value + try: + if index.column() == 0: + self._undo_stack.push( + SetNameCommand( + self._reach, index.row(), + self._reach.profile(index.row()).name, + value + ) ) - ) - if index.column() == 1: - self._undo_stack.push( - SetKPCommand( - self._reach, index.row(), - self._reach.profile(index.row()).kp, + if index.column() == 1: + self._undo_stack.push( + SetKPCommand( + self._reach, index.row(), + self._reach.profile(index.row()).kp, value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) self.layoutChanged.emit() diff --git a/src/View/Geometry/UndoCommand.py b/src/View/Geometry/UndoCommand.py index 148f7db3..1df9c58c 100644 --- a/src/View/Geometry/UndoCommand.py +++ b/src/View/Geometry/UndoCommand.py @@ -33,9 +33,13 @@ class SetDataCommand(QUndoCommand): self._reach = reach self._index = index self._old = old_value - self._new = new_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) + def undo(self): self._reach.profile(self._index).name = self._old @@ -43,6 +47,10 @@ class SetNameCommand(SetDataCommand): self._reach.profile(self._index).name = self._new class SetKPCommand(SetDataCommand): + def __init__(self, reach, index, old_value, new_value): + self.type = float + super(SetKPCommand, self).__init__(reach, index, old_value, new_value) + def undo(self): self._reach.profile(self._index).kp = self._old diff --git a/src/View/InitialConditions/Table.py b/src/View/InitialConditions/Table.py index ebb9af85..5e704ba9 100644 --- a/src/View/InitialConditions/Table.py +++ b/src/View/InitialConditions/Table.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +import logging +import traceback from tools import trace, timer from PyQt5.QtCore import ( @@ -39,6 +41,8 @@ from View.InitialConditions.UndoCommand import ( from View.InitialConditions.translate import * +logger = logging.getLogger() + _translate = QCoreApplication.translate class ComboBoxDelegate(QItemDelegate): @@ -135,12 +139,16 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._headers[column] is not None: - self._undo.push( - SetCommand( - self._ics, row, self._headers[column], value + try: + if self._headers[column] is not None: + self._undo.push( + SetCommand( + self._ics, row, self._headers[column], value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/InitialConditions/UndoCommand.py b/src/View/InitialConditions/UndoCommand.py index 9d93f78b..c96f90fa 100644 --- a/src/View/InitialConditions/UndoCommand.py +++ b/src/View/InitialConditions/UndoCommand.py @@ -34,7 +34,12 @@ class SetCommand(QUndoCommand): self._row = row self._column = column self._old = self._ics.get(self._row)[column] - self._new = new_value + + _type = float + if column == "name" or column == "comment": + _type = str + + self._new = _type(new_value) def undo(self): self._ics.get(self._row)[self._column] = self._old diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index 0fabd5cb..e9e878fd 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -17,6 +17,7 @@ # -*- coding: utf-8 -*- import logging +import traceback from datetime import date, time, datetime, timedelta from tools import trace, timer @@ -210,11 +211,15 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - self._undo.push( - SetDataCommand( - self._data, row, column, value + try: + self._undo.push( + SetDataCommand( + self._data, row, column, value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/LateralContribution/Edit/UndoCommand.py b/src/View/LateralContribution/Edit/UndoCommand.py index a0a602bc..715683c9 100644 --- a/src/View/LateralContribution/Edit/UndoCommand.py +++ b/src/View/LateralContribution/Edit/UndoCommand.py @@ -33,7 +33,8 @@ class SetDataCommand(QUndoCommand): self._index = index self._column = column self._old = self._data.get_i(self._index)[self._column] - self._new = new_value + _type = self._data.get_type_column(self._column) + self._new = _type(new_value) def undo(self): self._data._set_i_c_v(self._index, self._column, self._old) diff --git a/src/View/LateralContribution/Table.py b/src/View/LateralContribution/Table.py index f5a8bdf1..682bf00d 100644 --- a/src/View/LateralContribution/Table.py +++ b/src/View/LateralContribution/Table.py @@ -16,6 +16,9 @@ # -*- coding: utf-8 -*- +import logging +import traceback + from tools import trace, timer from PyQt5.QtCore import ( @@ -43,6 +46,8 @@ from Model.LateralContribution.LateralContributionTypes import ( ) from View.LateralContribution.translate import * +logger = logging.getLogger() + _translate = QCoreApplication.translate class ComboBoxDelegate(QItemDelegate): @@ -156,37 +161,41 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._headers[column] == "name": - self._undo.push( - SetNameCommand( - self._lcs, self._tab, row, value + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._lcs, self._tab, row, value + ) ) - ) - elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) - self._undo.push( - SetTypeCommand( - self._lcs, self._tab, row, LC_types[key] + elif self._headers[column] == "type": + key = next(k for k, v in long_types.items() if v == value) + self._undo.push( + SetTypeCommand( + self._lcs, self._tab, row, LC_types[key] + ) ) - ) - elif self._headers[column] == "edge": - self._undo.push( - SetEdgeCommand( - self._lcs, self._tab, row, self._data.edge(value) + elif self._headers[column] == "edge": + self._undo.push( + SetEdgeCommand( + self._lcs, self._tab, row, self._data.edge(value) + ) ) - ) - elif self._headers[column] == "begin_kp": - self._undo.push( - SetBeginCommand( - self._lcs, self._tab, row, value + elif self._headers[column] == "begin_kp": + self._undo.push( + SetBeginCommand( + self._lcs, self._tab, row, value + ) ) - ) - elif self._headers[column] == "end_kp": - self._undo.push( - SetEndCommand( - self._lcs, self._tab, row, value + elif self._headers[column] == "end_kp": + self._undo.push( + SetEndCommand( + self._lcs, self._tab, row, value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/LateralContribution/UndoCommand.py b/src/View/LateralContribution/UndoCommand.py index 6b688394..3d858436 100644 --- a/src/View/LateralContribution/UndoCommand.py +++ b/src/View/LateralContribution/UndoCommand.py @@ -34,7 +34,7 @@ class SetNameCommand(QUndoCommand): self._tab = tab self._index = index self._old = self._lcs.get(self._tab, self._index).name - self._new = new_value + self._new = str(new_value) def undo(self): self._lcs.get(self._tab, self._index).name = self._old @@ -50,7 +50,7 @@ class SetBeginCommand(QUndoCommand): self._tab = tab self._index = index self._old = self._lcs.get(self._tab, self._index).begin_kp - self._new = new_value + self._new = float(new_value) def undo(self): self._lcs.get(self._tab, self._index).begin_kp = float(self._old) @@ -66,7 +66,7 @@ class SetEndCommand(QUndoCommand): self._tab = tab self._index = index self._old = self._lcs.get(self._tab, self._index).end_kp - self._new = new_value + self._new = float(new_value) def undo(self): self._lcs.get(self._tab, self._index).end_kp = float(self._old) diff --git a/src/View/Network/Table.py b/src/View/Network/Table.py index 1b771779..b24865e8 100644 --- a/src/View/Network/Table.py +++ b/src/View/Network/Table.py @@ -16,6 +16,9 @@ # -*- coding: utf-8 -*- +import logging +import traceback + from Model.Network.Node import Node from Model.Network.Edge import Edge from Model.Network.Graph import Graph @@ -153,31 +156,35 @@ class GraphTableModel(QAbstractTableModel): def setData(self, index, value, role=Qt.EditRole): if index.isValid(): if role == Qt.EditRole: - if (self.headers[index.column()] == "node1" or - self.headers[index.column()] == "node2"): - node = self.graph.node(value) - self._undo.push( - SetNodeCommand( - self.graph, - self.rows[index.row()], - self.headers[index.column()], - node + try: + if (self.headers[index.column()] == "node1" or + self.headers[index.column()] == "node2"): + node = self.graph.node(value) + self._undo.push( + SetNodeCommand( + self.graph, + self.rows[index.row()], + self.headers[index.column()], + node + ) ) - ) - # elif self.headers[index.column()] == "enable": - # self._undo.push( - # EnableEdgeCommand( - # self.rows[index.row()], value - # ) - # ) - else: - self._undo.push( - SetCommand( - self.rows[index.row()], - self.headers[index.column()], - value + # elif self.headers[index.column()] == "enable": + # self._undo.push( + # EnableEdgeCommand( + # self.rows[index.row()], value + # ) + # ) + else: + self._undo.push( + SetCommand( + self.rows[index.row()], + self.headers[index.column()], + value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index, [Qt.DisplayRole]) self.layoutChanged.emit() diff --git a/src/View/SolverParameters/Table.py b/src/View/SolverParameters/Table.py index 9aa3b55b..5aa488ab 100644 --- a/src/View/SolverParameters/Table.py +++ b/src/View/SolverParameters/Table.py @@ -16,6 +16,9 @@ # -*- coding: utf-8 -*- +import logging +import traceback + from tools import trace, timer from PyQt5.QtCore import ( @@ -97,16 +100,20 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._headers[column] == "value": - if value in tr.r_yes_no: - value = tr.r_yes_no[value].lower() + try: + if self._headers[column] == "value": + if value in tr.r_yes_no: + value = tr.r_yes_no[value].lower() - self._undo.push( - SetCommand( - self._params, row, - "value", value + self._undo.push( + SetCommand( + self._params, row, + "value", value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/SolverParameters/UndoCommand.py b/src/View/SolverParameters/UndoCommand.py index 3c917e4f..d3343d00 100644 --- a/src/View/SolverParameters/UndoCommand.py +++ b/src/View/SolverParameters/UndoCommand.py @@ -33,7 +33,7 @@ class SetCommand(QUndoCommand): self._index = index self._column = column self._old = self._data.get(self._index)[column] - self._new = new_value + self._new = str(new_value) def undo(self): self._data.get(self._index)[self._column] = self._old diff --git a/src/View/Stricklers/Table.py b/src/View/Stricklers/Table.py index 31dd4d9b..5d32cda5 100644 --- a/src/View/Stricklers/Table.py +++ b/src/View/Stricklers/Table.py @@ -16,6 +16,9 @@ # -*- coding: utf-8 -*- +import logging +import traceback + from tools import trace, timer from PyQt5.QtCore import ( @@ -39,8 +42,9 @@ from View.Stricklers.UndoCommand import ( from View.Stricklers.translate import * -_translate = QCoreApplication.translate +logger = logging.getLogger() +_translate = QCoreApplication.translate class TableModel(QAbstractTableModel): def __init__(self, data=None, undo=None, tab=""): @@ -92,30 +96,34 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._headers[column] == "name": - self._undo.push( - SetNameCommand( - self._data, row, value + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._data, row, value + ) ) - ) - elif self._headers[column] == "comment": - self._undo.push( - SetCommentCommand( - self._data, row, value + elif self._headers[column] == "comment": + self._undo.push( + SetCommentCommand( + self._data, row, value + ) ) - ) - elif self._headers[column] == "minor": - self._undo.push( - SetMinorCommand( - self._data, row, value + elif self._headers[column] == "minor": + self._undo.push( + SetMinorCommand( + self._data, row, value + ) ) - ) - elif self._headers[column] == "medium": - self._undo.push( - SetMediumCommand( - self._data, row, value + elif self._headers[column] == "medium": + self._undo.push( + SetMediumCommand( + self._data, row, value + ) ) - ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) self.dataChanged.emit(index, index) return True diff --git a/src/View/Stricklers/UndoCommand.py b/src/View/Stricklers/UndoCommand.py index e7dc733e..ee75cf99 100644 --- a/src/View/Stricklers/UndoCommand.py +++ b/src/View/Stricklers/UndoCommand.py @@ -33,7 +33,7 @@ class SetNameCommand(QUndoCommand): self._data = data self._index = index self._old = self._data.get(self._index).name - self._new = new_value + self._new = str(new_value) def undo(self): self._data.get(self._index).name = self._old @@ -48,7 +48,7 @@ class SetCommentCommand(QUndoCommand): self._data = data self._index = index self._old = self._data.get(self._index).comment - self._new = new_value + self._new = str(new_value) def undo(self): self._data.get(self._index).comment = self._old @@ -63,7 +63,7 @@ class SetMinorCommand(QUndoCommand): self._data = data self._index = index self._old = self._data.get(self._index).minor - self._new = new_value + self._new = float(new_value) def undo(self): self._data.get(self._index).minor = self._old @@ -78,7 +78,7 @@ class SetMediumCommand(QUndoCommand): self._data = data self._index = index self._old = self._data.get(self._index).medium - self._new = new_value + self._new = float(new_value) def undo(self): self._data.get(self._index).medium = self._old From 61f31403d415819071dae5a55159170f21078f13 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 11:20:15 +0200 Subject: [PATCH 118/289] Solver: Add error message when command executable do not exists. --- src/Solver/ASolver.py | 23 +++++++++++++++++++++-- src/View/RunSolver/Window.py | 5 ++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index bfb47aa7..b4285923 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -193,6 +193,11 @@ class AbstractSolver(object): exe = cmd[0] args = cmd[1:] + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.info(error) + return error + self._process.start( exe, args, ) @@ -216,6 +221,11 @@ class AbstractSolver(object): exe = cmd[0] args = cmd[1:] + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.info(error) + return error + self._process.start( exe, args, ) @@ -240,6 +250,11 @@ class AbstractSolver(object): exe = cmd[0] args = cmd[1:] + if not os.path.exists(exe): + error = f"[ERROR] Path {exe} do not exists" + logger.info(error) + return error + self._process.start( exe, args, ) @@ -255,7 +270,9 @@ class AbstractSolver(object): def _run_next(self): self._step += 1 if self._step < len(self._runs): - self._runs[self._step]() + res = self._runs[self._step]() + if res is not True: + self._output.put(res) else: self._status = STATUS.STOPED @@ -281,7 +298,9 @@ class AbstractSolver(object): ] self._step = 0 # Run first step - self._runs[0]() + res = self._runs[0]() + if res is not True: + self._output.put(res) def kill(self): if self._process is None: diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index dba5a16e..4f915950 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -198,7 +198,10 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): while self._output.qsize() != 0: s = self._output.get() - self._log(s) + if type(s) is str and "[ERROR]" in s: + self._log(s, color="red") + else: + self._log(s) def start(self): if self._solver.is_stoped(): From ec16586755ba2e74d3bd0857864e7ceacd2975a9 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 15:00:23 +0200 Subject: [PATCH 119/289] Geometry, ListedSubWindow: Use ListedSubWindow in Geometry windows. --- src/View/ASubWindow.py | 10 ++++++---- src/View/Geometry/Profile/Window.py | 12 ++++++++---- src/View/Geometry/Window.py | 13 +++++++++---- src/View/ListedSubWindow.py | 25 ++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/View/ASubWindow.py b/src/View/ASubWindow.py index c07465f1..d9e121f4 100644 --- a/src/View/ASubWindow.py +++ b/src/View/ASubWindow.py @@ -462,10 +462,12 @@ class ASubWindowFeatures(object): class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): def __init__(self, name="", ui="dummy", parent=None): super(ASubMainWindow, self).__init__(parent=parent) - self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", f"{ui}.ui"), - self - ) + if ui is not None: + self.ui = loadUi( + os.path.join(os.path.dirname(__file__), "ui", f"{ui}.ui"), + self + ) + self.name = name self.parent = parent if self.parent is not None: diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index cfcf6925..9f7df090 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( from Model.Geometry.Reach import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ -from View.ASubWindow import WindowToolKit +from View.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 * @@ -45,10 +45,14 @@ from View.Geometry.Profile.Table import * _translate = QCoreApplication.translate -class ProfileWindow(QMainWindow, WindowToolKit): - def __init__(self, profile=None, parent=None): +class ProfileWindow(ASubMainWindow): + def __init__(self, profile=None, title="Profile", parent=None): + self._title = title self.parent = parent - super(ProfileWindow, self).__init__(self.parent) + super(ProfileWindow, self).__init__( + name=self._title, + parent=self.parent + ) self.ui = Ui_MainWindow() self.ui.setupUi(self) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 9fd0f6f2..46e339f6 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -41,7 +41,8 @@ from View.Geometry.PlotXY import PlotXY from View.Geometry.PlotKPC import PlotKPC from View.Geometry.PlotAC import PlotAC -from View.ASubWindow import WindowToolKit +from View.ASubWindow import ASubMainWindow, WindowToolKit +from View.ListedSubWindow import ListedSubWindow from View.Geometry.mainwindow_ui_reach import Ui_MainWindow from View.Geometry.Table import * from View.Geometry.Profile.Window import ProfileWindow @@ -49,10 +50,14 @@ from View.Geometry.Profile.Window import ProfileWindow _translate = QCoreApplication.translate -class GeometryWindow(QMainWindow, WindowToolKit): - def __init__(self, model=None, parent=None): +class GeometryWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, model=None, title="Geometry", parent=None): + self._title = title self.parent = parent - super(GeometryWindow, self).__init__(parent=parent) + super(GeometryWindow, self).__init__( + name=self._title, + parent=parent + ) self._model = model self._reach = model.river.current_reach().reach diff --git a/src/View/ListedSubWindow.py b/src/View/ListedSubWindow.py index 37d64234..eaef39ef 100644 --- a/src/View/ListedSubWindow.py +++ b/src/View/ListedSubWindow.py @@ -48,9 +48,32 @@ class ListedSubWindow(object): self.sub_win_cnt = len(self.sub_win_list) logger.info(f"Close window: {name} ({self.sub_win_cnt})") - def sub_win_exists(self, name): + def _sub_win_exists(self, name): return reduce( lambda acc, n: (acc or (n[0] == name)), self.sub_win_list, False ) + + def _sub_win_exists_with_contain(self, name, contain): + return reduce( + lambda acc, n: ( + acc or + ( + (n[0] == name) and + reduce( + lambda acc, a: acc and (a in n[1]._title), + contain, + True + ) + ) + ), + self.sub_win_list, + False + ) + + def sub_win_exists(self, name, contain = []): + if contain == []: + self._sub_win_exists(name) + else: + self._sub_win_exists_with_contain(name, contain) From ec9a061f9ad2502a5241eb1762709d0521cd4df6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 15:28:01 +0200 Subject: [PATCH 120/289] Geometry: Forbidden duplicate window. --- src/View/Geometry/Profile/Window.py | 4 +++- src/View/Geometry/Window.py | 3 ++- src/View/ListedSubWindow.py | 24 +++++++++++++++++++++--- src/View/MainWindow.py | 15 +++++++++++---- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 9f7df090..54d1e011 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -86,12 +86,14 @@ class ProfileWindow(ASubMainWindow): if (name is None) or (name == ""): name = _translate("MainWindowProfile", "(no name)") - self.setWindowTitle( + self._title = ( header + " - " + f"{self._profile.reach.name}" + " - " + f"{name} ({self._profile.kp})" ) + self.setWindowTitle(self._title) + def setup_sc(self): self._undo_stack = QUndoStack() diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 46e339f6..851b73bb 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -80,7 +80,8 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.changed_slider_value() def setup_window(self): - self.setWindowTitle(f"{self.ui.mainwindow_title} - {self._reach.name}") + self._title = f"{self.ui.mainwindow_title} - {self._reach.name}" + self.setWindowTitle(self._title) def setup_sc(self): self._undo_stack = QUndoStack() diff --git a/src/View/ListedSubWindow.py b/src/View/ListedSubWindow.py index eaef39ef..b44efd61 100644 --- a/src/View/ListedSubWindow.py +++ b/src/View/ListedSubWindow.py @@ -62,7 +62,7 @@ class ListedSubWindow(object): ( (n[0] == name) and reduce( - lambda acc, a: acc and (a in n[1]._title), + lambda acc, c: acc and (c in n[1]._title), contain, True ) @@ -74,6 +74,24 @@ class ListedSubWindow(object): def sub_win_exists(self, name, contain = []): if contain == []: - self._sub_win_exists(name) + return self._sub_win_exists(name) else: - self._sub_win_exists_with_contain(name, contain) + return self._sub_win_exists_with_contain(name, contain) + + def sub_win_filter_first(self, name, contain): + try: + return next( + filter( + lambda n: ( + (n[0] == name) and + reduce( + lambda acc, c: acc and (c in n[1]._title), + contain, + True + ) + ), + self.sub_win_list, + ) + )[1] + except: + return None diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 8a94c469..7b03dfb7 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -455,10 +455,17 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if (self.model is not None and - self.model.river.has_current_reach()): - geometry = GeometryWindow(model=self.model, parent=self) - geometry.show() + if (self.model is not None and self.model.river.has_current_reach()): + geometry = self.sub_win_filter_first( + "Geometry", + contain = [self.model.river.current_reach().name] + ) + + if geometry == None: + geometry = GeometryWindow(model=self.model, parent=self) + geometry.show() + else: + geometry.activateWindow() else: self.msg_select_reach() From 289c8ed12923b4f3fd03d22a509d8c2e5c165623 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 15:32:09 +0200 Subject: [PATCH 121/289] Geometry: Profile: Forbidden duplicate window for profile editing. --- src/View/Geometry/Window.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 851b73bb..e33b6d54 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -184,13 +184,20 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): for row in rows: profile = self._reach.profile(row) - win = ProfileWindow( - profile = profile, - parent = self, + win = self.sub_win_filter_first( + "Profile", + contain = [self._reach.name, str(profile.kp)] ) - self._profile_window.append(win) - win.show() + if win is None: + win = ProfileWindow( + profile = profile, + parent = self, + ) + self._profile_window.append(win) + win.show() + else: + win.activateWindow() self.tableView.model().blockSignals(False) From 94a0a9665936df6c56eb6bda9216943c8d524dbf Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 15:55:25 +0200 Subject: [PATCH 122/289] BC, LC: Forbidden duplicate window. --- src/View/BoundaryCondition/Edit/Window.py | 2 +- src/View/BoundaryCondition/Window.py | 22 +++++++++++------ src/View/LateralContribution/Edit/Window.py | 8 +++---- src/View/LateralContribution/Window.py | 22 +++++++++++------ src/View/ListedSubWindow.py | 2 +- src/View/MainWindow.py | 26 +++++++++++++++++---- 6 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 9ecd240c..efcf0993 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -71,7 +71,7 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self._title = ( _translate("Edit boundary condition", self._title) + f" - {self._study.name} " + - f" - {self._data.name} " + + f" - {self._data.name} ({self._data.id}) " + f"({long_types[self._data.bctype]} - {node_name})" ) diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index af314574..a2853590 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -65,7 +65,7 @@ logger = logging.getLogger() class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Boundary conditions", study=None, parent=None): - title = title + " - " + study.name + self._title = title + " - " + study.name super(BoundaryConditionWindow, self).__init__( name=title, ui="BoundaryConditions", parent=parent @@ -79,7 +79,7 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(title) + self.ui.setWindowTitle(self._title) def setup_sc(self): self._undo_stack = QUndoStack() @@ -220,9 +220,17 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): tab = self.current_tab() rows = self.index_selected_rows() for row in rows: - win = EditBoundaryConditionWindow( - data=self._bcs.get(tab, row), - study=self._study, - parent=self + win = self.sub_win_filter_first( + "Edit boundary condition", + contain = [f"({self._bcs.get(tab, row).id})"] ) - win.show() + + if win is None: + win = EditBoundaryConditionWindow( + data=self._bcs.get(tab, row), + study=self._study, + parent=self + ) + win.show() + else: + win.activateWindow() diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index c3459958..3a801591 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -45,7 +45,7 @@ from View.LateralContribution.Edit.Plot import Plot _translate = QCoreApplication.translate class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit lateral conditribution", + def __init__(self, title="Edit lateral contribution", data=None, study=None, parent=None): self._data = data self._study = study @@ -54,7 +54,7 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self.compute_title() super(EditLateralContributionWindow, self).__init__( - name=self._title, ui="EditLateralContribution", parent=parent + name=title, ui="EditLateralContribution", parent=parent ) self.ui.setWindowTitle(self._title) @@ -69,9 +69,9 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): 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) + + _translate("Edit lateral contribution", self._title) + f" - {self._study.name} " + - f" - {self._data.name} " + + f" - {self._data.name} ({self._data.id}) " + f"({long_types[self._data.lctype]} - {edge_name})" ) diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index 003ad48e..dc3dd94f 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -65,7 +65,7 @@ logger = logging.getLogger() class LateralContributionWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Lateral contribution", study=None, parent=None): - title = title + " - " + study.name + self._title = title + " - " + study.name super(LateralContributionWindow, self).__init__( name=title, ui="LateralContributions", parent=parent @@ -79,7 +79,7 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(title) + self.ui.setWindowTitle(self._title) def setup_sc(self): self._undo_stack = QUndoStack() @@ -266,9 +266,17 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): tab = self.current_tab() rows = self.index_selected_rows() for row in rows: - win = EditLateralContributionWindow( - data=self._lcs.get(tab, row), - study=self._study, - parent=self + win = self.sub_win_filter_first( + "Edit lateral contribution", + contain = [f"({self._lcs.get(tab, row).id})"] ) - win.show() + + if win is None: + win = EditLateralContributionWindow( + data=self._lcs.get(tab, row), + study=self._study, + parent=self + ) + win.show() + else: + win.activateWindow() diff --git a/src/View/ListedSubWindow.py b/src/View/ListedSubWindow.py index b44efd61..b4dfe416 100644 --- a/src/View/ListedSubWindow.py +++ b/src/View/ListedSubWindow.py @@ -83,7 +83,7 @@ class ListedSubWindow(object): return next( filter( lambda n: ( - (n[0] == name) and + (name in n[0]) and reduce( lambda acc, c: acc and (c in n[1]._title), contain, diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 7b03dfb7..8b028d1b 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -461,7 +461,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): contain = [self.model.river.current_reach().name] ) - if geometry == None: + if geometry is None: geometry = GeometryWindow(model=self.model, parent=self) geometry.show() else: @@ -470,12 +470,28 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.msg_select_reach() def open_boundary_cond(self): - bound = BoundaryConditionWindow(study = self.model, parent = self) - bound.show() + bound = self.sub_win_filter_first( + "Boundary conditions", + contain = [] + ) + + if bound is None: + bound = BoundaryConditionWindow(study = self.model, parent = self) + bound.show() + else: + bound.activateWindow() def open_lateral_contrib(self): - lateral = LateralContributionWindow(study = self.model, parent = self) - lateral.show() + lateral = self.sub_win_filter_first( + "Lateral contribution", + contain = [] + ) + + if lateral is None: + lateral = LateralContributionWindow(study = self.model, parent = self) + lateral.show() + else: + lateral.activateWindow() def open_stricklers(self): strick = StricklersWindow( From 254f0d0e8e78b928a68f03a76277ccc6822ffd3f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 16:17:18 +0200 Subject: [PATCH 123/289] Friction: Forbidden duplicate window. --- src/View/Frictions/Window.py | 20 ++++++++++++++------ src/View/MainWindow.py | 35 ++++++++++++++++++++++++++--------- src/View/Stricklers/Window.py | 4 ++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index d5761485..81049593 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -68,7 +68,7 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): self.setup_title(title) super(FrictionsWindow, self).__init__( - name=self._title, ui="Frictions", parent=parent + name=title, ui="Frictions", parent=parent ) self.setup_sc() @@ -246,9 +246,17 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): self._table.redo() def edit_stricklers(self): - self.strick = StricklersWindow( - study = self._study, - config = self.parent.conf, - parent = self + strick = self.sub_win_filter_first( + "Stricklers", + contain = [] ) - self.strick.show() + + if strick is None: + strick = StricklersWindow( + study = self._study, + config = self.parent.conf, + parent = self + ) + strick.show() + else: + strick.activateWindow() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 8b028d1b..08c86628 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -494,21 +494,38 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): lateral.activateWindow() def open_stricklers(self): - strick = StricklersWindow( - study = self.model, - config = self.conf, - parent = self + strick = self.sub_win_filter_first( + "Stricklers", + contain = [] ) - strick.show() + + if strick is None: + strick = StricklersWindow( + study = self.model, + config = self.conf, + parent = self + ) + strick.show() + else: + strick.activateWindow() def open_frictions(self): if (self.model is not None and self.model.river.has_current_reach()): - frictions = FrictionsWindow( - study = self.model, - parent = self + + frictions = self.sub_win_filter_first( + "Frictions", + contain = [self.model.river.current_reach().name] ) - frictions.show() + + if frictions is None: + frictions = FrictionsWindow( + study = self.model, + parent = self + ) + frictions.show() + else: + frictions.activateWindow() else: self.msg_select_reach() diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index 29f886cb..8726f971 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -51,7 +51,7 @@ logger = logging.getLogger() class StricklersWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="Stricklers", study=None, config=None, parent=None): - title = title + " - " + study.name + self._title = title + " - " + study.name super(StricklersWindow, self).__init__( name=title, ui="Stricklers", parent=parent @@ -64,7 +64,7 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): self.setup_table() self.setup_connections() - self.ui.setWindowTitle(title) + self.ui.setWindowTitle(self._title) def setup_sc(self): self._undo_stack = QUndoStack() From f4a852f8f3d33e2953461da21c60f4f2e69a8185 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 16:37:14 +0200 Subject: [PATCH 124/289] SolverParameters: Forbidden duplicate window. --- src/View/MainWindow.py | 16 ++++++++++++---- src/View/SolverParameters/Window.py | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 08c86628..ad2e1c3c 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -540,11 +540,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.msg_select_reach() def open_solver_parameters(self): - params = SolverParametersWindow( - study = self.model, - parent = self + params = self.sub_win_filter_first( + "Solver parameters", + contain = [] ) - params.show() + + if params is None: + params = SolverParametersWindow( + study = self.model, + parent = self + ) + params.show() + else: + params.activateWindow() def run_solver(self): if self.model is None: diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index 9560251d..c04eebc7 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -56,7 +56,7 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): # Init tanslate dictionary tr.init() - title = title + " - " + study.name + self._title = title + " - " + study.name super(SolverParametersWindow, self).__init__( name=title, ui="SolverParameters", parent=parent @@ -69,7 +69,7 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): self.setup_table() self.setup_connections() - self.ui.setWindowTitle(title) + self.ui.setWindowTitle(self._title) def setup_sc(self): self._undo_stack = {} From 06b6d59494e4e0bfbd48a76c13135f5731ed31c8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 16:40:37 +0200 Subject: [PATCH 125/289] InitialConditions: Forbidden duplicate window. --- src/View/MainWindow.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index ad2e1c3c..0808ba8f 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -531,11 +531,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_initial_conditions(self): if self.model.river.has_current_reach(): - initial = InitialConditionsWindow( - study = self.model, - parent = self + initial = self.sub_win_filter_first( + "Initial condition", + contain = [self.model.river.current_reach().name] ) - initial.show() + + if initial is None: + initial = InitialConditionsWindow( + study = self.model, + parent = self + ) + initial.show() + else: + initial.activateWindow() else: self.msg_select_reach() From 02dafd469462f574f8325b392f74a10e0a4b7dd1 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 4 Aug 2023 17:17:42 +0200 Subject: [PATCH 126/289] Solver: Fix case of space into solver path. --- src/Solver/ASolver.py | 45 +++++++++++++++++++++++++++++++++---------- src/config.py | 2 +- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index b4285923..ca6e6f4f 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -189,9 +189,18 @@ class AbstractSolver(object): logger.debug(f"! {cmd}") - cmd = cmd.split() - exe = cmd[0] - args = cmd[1:] + if cmd[0] == "\"": + # Command line executable path is between " char + cmd = cmd.split("\"") + exe = cmd[1] + args = "\"".join(cmd[3:]).split(" ") + else: + # We suppose the command line executable path as no space char + cmd = cmd.replace("\ ", "&_&").split(" ") + exe = cmd[0].replace("&_&", "\ ") + args = cmd[1:] + + logger.info(f"! {exe} {args}") if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -215,11 +224,20 @@ class AbstractSolver(object): cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) - logger.debug(f"! {cmd}") + logger.info(f"! {cmd}") - cmd = cmd.split() - exe = cmd[0] - args = cmd[1:] + if cmd[0] == "\"": + # Command line executable path is between " char + cmd = cmd.split("\"") + exe = cmd[1] + args = "\"".join(cmd[3:]).split(" ") + else: + # We suppose the command line executable path as no space char + cmd = cmd.replace("\ ", "&_&").split(" ") + exe = cmd[0].replace("&_&", "\ ") + args = cmd[1:] + + logger.info(f"! {exe} {args}") if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -246,9 +264,16 @@ class AbstractSolver(object): logger.debug(f"! {cmd}") - cmd = cmd.split() - exe = cmd[0] - args = cmd[1:] + if cmd[0] == "\"": + # Command line executable path is between " char + cmd = cmd.split("\"") + exe = cmd[1] + args = "\"".join(cmd[3:]).split(" ") + else: + # We suppose the command line executable path as no space char + cmd = cmd.replace("\ ", "&_&").split(" ") + exe = cmd[0].replace("&_&", "\ ") + args = cmd[1:] if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" diff --git a/src/config.py b/src/config.py index 16ec6b12..89563ecb 100644 --- a/src/config.py +++ b/src/config.py @@ -254,7 +254,7 @@ class Config(SQL): ctor = solver_type_list["mage8"] new = ctor("default-mage") new._description = "Default PAMHYR mage 8 version" - new._cmd_solver = f"""@install_dir/mage/mage{"" if posix else ".exe"} -fp=1 @input""" + new._cmd_solver = f""""@install_dir/mage/mage{"" if posix else ".exe"}" -fp=1 @input""" self._solvers.append(new) # Meshing tool From 9ecd9a8f2a138becf2e7ae9144716cfe285b2839 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 7 Aug 2023 11:20:22 +0200 Subject: [PATCH 127/289] Config: Save the last study open and keep close status. --- src/View/ASubWindow.py | 5 +++- src/View/MainWindow.py | 52 +++++++++++++++++++++++++++++++++++++++--- src/config.py | 30 +++++++++++++++++++++++- 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/View/ASubWindow.py b/src/View/ASubWindow.py index d9e121f4..4e1c1327 100644 --- a/src/View/ASubWindow.py +++ b/src/View/ASubWindow.py @@ -78,13 +78,14 @@ class WindowToolKit(object): return header, values - def file_dialog(self, select_file=True, callback=lambda x: None): + def file_dialog(self, select_file=True, callback=lambda x: None, directory=None): """Open a new file dialog and send result to callback function Args: select_file: Select a file if True, else select a dir callback: The callback function with one arguments, files selection list + directory: Defaut directory Returns: The returns of callback @@ -97,6 +98,8 @@ class WindowToolKit(object): mode = QFileDialog.FileMode.Directory dialog.setFileMode(mode) + if directory is not None: + dialog.setDirectory(directory) if dialog.exec_(): file_names = dialog.selectedFiles() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 0808ba8f..afb2de1e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -119,6 +119,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.trans = QTranslator(self) #self.ui.retranslateUi() + if self.conf.last_study != "" and not self.conf.close_correctly: + self.dialog_reopen_study() + def set_title(self): if self.model is not None: self.setWindowTitle(f"PAMHYR - {self.model.name}") @@ -202,16 +205,24 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self.model is not None and not self.model.is_saved: self._close_question = True if self.dialog_close(): + # PAMHYR is close correctly (no crash) + self.conf.set_close_correctly() + super(ApplicationWindow, self).close() else: self._close_question = False else: + # PAMHYR is close correctly (no crash) + self.conf.set_close_correctly() + super(ApplicationWindow, self).close() def closeEvent(self, event): if not self._close_question: if self.model is not None and not self.model.is_saved: if self.dialog_close(cancel = False): + # PAMHYR is close correctly (no crash) + self.conf.set_close_correctly() super(ApplicationWindow, self).closeEvent(event) else: super(ApplicationWindow, self).closeEvent(event) @@ -264,11 +275,13 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def set_model(self, model): self.model = model self.update_enable_action() + self.conf.set_last_study(self.model.filename) self.set_title() def close_model(self): self.model = None self.update_enable_action() + self.conf.set_close_correctly() self.set_title() def update_enable_action(self): @@ -295,6 +308,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # FEATURES # ############ + def open_study(self, filename): + """Open a study + + Args: + filename: The study path + + Returns: + Nothing + """ + self.set_model(Study.open(filename)) + logger.info(f"Open Study - {self.model.name}") + self.set_title() + def save_study(self): """Save current study @@ -351,6 +377,27 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "Geometry edition need a reach selected " "into river network window to work on it") + def dialog_reopen_study(self): + dlg = QMessageBox(self) + + dlg.setWindowTitle("Last open study") + dlg.setText("Do you want to open again the last open study?") + opt = QMessageBox.Cancel | QMessageBox.Ok #| QMessageBox.Open + + dlg.setStandardButtons(opt) + dlg.setIcon(QMessageBox.Question) + + res = dlg.exec() + + if res == QMessageBox.Ok: + self.open_study(self.conf.last_study) + return True + elif res == QMessageBox.Open: + self.open_model() + return True + elif res == QMessageBox.Cancel: + return False + def dialog_close(self, cancel = True): dlg = QMessageBox(self) @@ -411,12 +458,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): dialog.setDefaultSuffix(".pamhyr") #dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden) dialog.setNameFilters(['PamHyr (*.pamhyr)']) + dialog.setDirectory(os.path.dirname(self.conf.last_study)) if dialog.exec_(): file_name = dialog.selectedFiles() - self.set_model(Study.open(file_name[0])) - logger.info(f"Open Study - {self.model.name}") - self.set_title() + self.open_study(file_name[0]) def open_new_study(self): """Open dialog to set new study diff --git a/src/config.py b/src/config.py index 89563ecb..d1ebb71e 100644 --- a/src/config.py +++ b/src/config.py @@ -34,7 +34,7 @@ logger = logging.getLogger() class Config(SQL): def __init__(self): - self._version = '0.0.2' + self._version = '0.0.3' self.filename = Config.filename() self.set_default_value() @@ -105,6 +105,10 @@ class Config(SQL): '' ) """) + if int(release) < 3: + self.execute(f"INSERT INTO data VALUES ('last_study', '')") + self.execute(f"INSERT INTO data VALUES ('close_correctly', 'True')") + self.commit() def _load_solver(self): @@ -176,6 +180,12 @@ class Config(SQL): v = self.execute("SELECT value FROM data WHERE key='lang'") self.lang = v[0] + # Last study + v = self.execute("SELECT value FROM data WHERE key='last_study'") + self.last_study = v[0] + v = self.execute("SELECT value FROM data WHERE key='close_correctly'") + self.close_correctly = v[0] == "True" + # Debug v = self.execute("SELECT value FROM data WHERE key='debug'") self.debug = v[0] == "True" @@ -228,6 +238,8 @@ class Config(SQL): "backup_max": self.backup_max, "editor": self.editor, "lang": self.lang, + "last_study": self.last_study, + "close_correctly": self.close_correctly, "debug": self.debug, } @@ -279,9 +291,25 @@ class Config(SQL): # Stricklers self.stricklers = StricklersList() + # Last study + self.last_study = "" + self.close_correctly = False + # Debug self.debug = False + def set_close_correctly(self): + self.close_correctly = True + 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.commit() + @classmethod def filename(cls): file = "" From 0835900aad02a864b846d0f8180f28822b323c82 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 7 Aug 2023 12:04:18 +0200 Subject: [PATCH 128/289] BC, LC: Fix convert exceptions. --- src/Model/BoundaryCondition/BoundaryCondition.py | 11 +++++++++-- src/Model/LateralContribution/LateralContribution.py | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index 7df962d2..86e0f1d2 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -16,11 +16,15 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer, old_pamhyr_date_to_timestamp from Model.DB import SQLSubModel from Model.Except import NotImplementedMethodeError +logger = logging.getLogger() + class BoundaryCondition(SQLSubModel): _sub_classes = [] _id_cnt = 0 @@ -317,14 +321,17 @@ class BoundaryCondition(SQLSubModel): new = cls(name = self.name, status = self._status) new.node = self.node - for i, _ in self.data: + for i, _ in enumerate(self.data): new.add(i) for i in [0,1]: for j in [0,1]: if self._header[i] == new.header[j]: for ind, v in self.data: - new._set_i_c_v(ind, j, v[i]) + try: + new._set_i_c_v(ind, j, v[i]) + except Exception as e: + logger.info(e) return new diff --git a/src/Model/LateralContribution/LateralContribution.py b/src/Model/LateralContribution/LateralContribution.py index e95733bf..38cbada4 100644 --- a/src/Model/LateralContribution/LateralContribution.py +++ b/src/Model/LateralContribution/LateralContribution.py @@ -16,11 +16,15 @@ # -*- coding: utf-8 -*- +import logging + from tools import trace, timer, old_pamhyr_date_to_timestamp from Model.DB import SQLSubModel from Model.Except import NotImplementedMethodeError +logger = logging.getLogger() + class LateralContribution(SQLSubModel): _sub_classes = [] _id_cnt = 0 @@ -358,14 +362,17 @@ class LateralContribution(SQLSubModel): new.begin_kp = self.begin_kp new.end_kp = self.end_kp - for i, _ in self.data: + for i, _ in enumerate(self.data): new.add(i) for i in [0,1]: for j in [0,1]: if self._header[i] == new.header[j]: for ind, v in self.data: - new._set_i_c_v(ind, j, v[i]) + try: + new._set_i_c_v(ind, j, v[i]) + except Exception as e: + logger.info(e) self._status.modified() return new From ac12275d81630839a1712e6f84b808f0461a809f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 7 Aug 2023 13:39:33 +0200 Subject: [PATCH 129/289] git: Remove submodule mage. --- .gitmodules | 3 --- mage | 1 - 2 files changed, 4 deletions(-) delete mode 160000 mage diff --git a/.gitmodules b/.gitmodules index 0aad7c1d..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "mage"] - path = mage - url = https://gitlab.irstea.fr/jean-baptiste.faure/mage.git diff --git a/mage b/mage deleted file mode 160000 index 18aa9a7e..00000000 --- a/mage +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 18aa9a7e0f8415a94a40fe647d329268c6932663 From 475c9fb23c0b2062dd80fdef8c659b51a6ae210c Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 7 Aug 2023 14:26:33 +0200 Subject: [PATCH 130/289] Pamhyr: Add log file and some log display. --- src/pamhyr.py | 38 ++++++++++++++++++++++++++++++++++++-- src/tools.py | 24 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/pamhyr.py b/src/pamhyr.py index 0f957d70..1c7ec5ca 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -27,7 +27,8 @@ from PyQt5.QtWidgets import QApplication from config import Config from tools import ( - reset_timers, display_timers, timer + reset_timers, display_timers, timer, + logger_color_blue, logger_color_red, logger_color_green, logger_color_reset ) from View.MainWindow import ApplicationWindow @@ -35,12 +36,45 @@ from Model.Study import Study logging.basicConfig( level=logging.DEBUG, - format='[PAMHYR][%(levelname)s] %(message)s' + format=(f'[{logger_color_blue()}PAMHYR{logger_color_reset()}]' + + f'[{logger_color_green()}%(levelname)s{logger_color_reset()}]' + + ' %(message)s') ) logger = logging.getLogger() logger.setLevel(logging.INFO) +try: + log = os.path.join( + os.path.dirname(Config.filename()), "log.txt" + ) + logfile = open(log, "w+") + handler = logging.StreamHandler(logfile) + formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s') + handler.setFormatter(formatter) + handler.setLevel(logging.DEBUG) + logger.addHandler(handler) +except: + logger.error("Failed to create logfile...") + def license(): + blue = lambda s: 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.""") + blue(""" MM ,M9 ,V^MM. M YM ,M MM MM MM VMA ,V MM ,M9""") + blue(""" MMmmdM9 ,M `MM M Mb M' MM MMmmmmmmMM VMMP MMmmdM9""") + blue(""" MM AbmmmqMA M YM.P' MM MM MM MM MM YM.""") + blue(""" MM A' VML M `YM' MM MM MM MM MM `Mb.""") + blue(""".JMML. .AMA. .AMMA..JML. `' .JMML..JMML. .JMML. .JMML. .JMML. .JMM.""") + with open(os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "VERSION" + ) + ), "r") as f: + version = f.readline().strip() + 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") diff --git a/src/tools.py b/src/tools.py index d5707c56..c345fb7f 100644 --- a/src/tools.py +++ b/src/tools.py @@ -39,6 +39,30 @@ from functools import ( logger = logging.getLogger() +def logger_color_blue(): + posix = os.name == "posix" + if posix: + return f"{Style.BRIGHT}{Fore.BLUE}" + return "" + +def logger_color_red(): + posix = os.name == "posix" + if posix: + return f"{Style.BRIGHT}{Fore.RED}" + return "" + +def logger_color_green(): + posix = os.name == "posix" + if posix: + return f"{Style.BRIGHT}{Fore.GREEN}" + return "" + +def logger_color_reset(): + posix = os.name == "posix" + if posix: + return f"{Style.RESET_ALL}" + return "" + ########## # TIMERS # ########## From cecbb6817726cb25fcceae6940eec57c8e68bd40 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 7 Aug 2023 16:03:04 +0200 Subject: [PATCH 131/289] Solver: Fix command line parsing. --- src/Solver/ASolver.py | 62 ++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 42 deletions(-) diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index ca6e6f4f..d438af05 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -176,12 +176,15 @@ class AbstractSolver(object): ) ) - def run_input_data_fomater(self): - if self._cmd_input == "": - self._run_next() - return True + def _format_command(self, cmd): + """Format command line - cmd = self._cmd_input + Args: + cmd: The command line + + Returns: + The executable and list of arguments + """ cmd = cmd.replace("@install_dir", self._install_dir()) cmd = cmd.replace("@path", self._path_input) cmd = cmd.replace("@input", self.input_param()) @@ -193,7 +196,7 @@ class AbstractSolver(object): # Command line executable path is between " char cmd = cmd.split("\"") exe = cmd[1] - args = "\"".join(cmd[3:]).split(" ") + args = "\"".join(cmd[2:]).split(" ")[1:] else: # We suppose the command line executable path as no space char cmd = cmd.replace("\ ", "&_&").split(" ") @@ -201,6 +204,15 @@ class AbstractSolver(object): args = cmd[1:] logger.info(f"! {exe} {args}") + return exe, args + + def run_input_data_fomater(self): + if self._cmd_input == "": + self._run_next() + return True + + cmd = self._cmd_input + exe, args = self._format_command(cmd) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -219,25 +231,7 @@ class AbstractSolver(object): return True cmd = self._cmd_solver - cmd = cmd.replace("@install_dir", self._install_dir()) - cmd = cmd.replace("@path", self._path_solver) - cmd = cmd.replace("@input", self.input_param()) - cmd = cmd.replace("@dir", self._process.workingDirectory()) - - logger.info(f"! {cmd}") - - if cmd[0] == "\"": - # Command line executable path is between " char - cmd = cmd.split("\"") - exe = cmd[1] - args = "\"".join(cmd[3:]).split(" ") - else: - # We suppose the command line executable path as no space char - cmd = cmd.replace("\ ", "&_&").split(" ") - exe = cmd[0].replace("&_&", "\ ") - args = cmd[1:] - - logger.info(f"! {exe} {args}") + exe, args = self._format_command(cmd) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -257,23 +251,7 @@ class AbstractSolver(object): return True cmd = self._cmd_output - cmd = cmd.replace("@install_dir", self._install_dir()) - cmd = cmd.replace("@path", self._path_output) - cmd = cmd.replace("@input", self.input_param()) - cmd = cmd.replace("@dir", self._process.workingDirectory()) - - logger.debug(f"! {cmd}") - - if cmd[0] == "\"": - # Command line executable path is between " char - cmd = cmd.split("\"") - exe = cmd[1] - args = "\"".join(cmd[3:]).split(" ") - else: - # We suppose the command line executable path as no space char - cmd = cmd.replace("\ ", "&_&").split(" ") - exe = cmd[0].replace("&_&", "\ ") - args = cmd[1:] + exe, args = self._format_command(cmd) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" From dcc6e2df2d3ba1864a60151f50b8336f839e82c6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 7 Aug 2023 16:03:32 +0200 Subject: [PATCH 132/289] Network: Fix window duplication prohibition mechanism. --- src/View/MainWindow.py | 10 ++++++---- src/View/Network/Window.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index afb2de1e..d5508a10 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -490,10 +490,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if (self.model is not None and - not self.sub_win_exists("River network")): - self.network = NetworkWindow(model=self.model, parent=self) - self.network.show() + if self.model is not None: + if not self.sub_win_exists("River network"): + self.network = NetworkWindow(model=self.model, parent=self) + self.network.show() + else: + self.network.activateWindow() def open_geometry(self): """Open geometry window diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index 7099cea5..05051862 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -50,7 +50,7 @@ class NetworkWindow(ASubMainWindow): self.setup_title() super(NetworkWindow, self).__init__( - name=self._title, ui="Network", parent=parent + name=title, ui="Network", parent=parent ) self.ui.setWindowTitle(self._title) From 798d9198d6fca5014a9b58d89fa5a4de1923c011 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 7 Aug 2023 16:20:38 +0200 Subject: [PATCH 133/289] Solver: Fix command line parsing (again). --- src/Solver/ASolver.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index d438af05..300bb36c 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -176,17 +176,20 @@ class AbstractSolver(object): ) ) - def _format_command(self, cmd): + def _format_command(self, cmd, path = ""): """Format command line Args: cmd: The command line + path: Optional path string (replace @path in cmd) Returns: The executable and list of arguments """ + # HACK: Works in most case... Trust me i'm an engineer + cmd = cmd.replace("@install_dir", self._install_dir()) - cmd = cmd.replace("@path", self._path_input) + cmd = cmd.replace("@path", path.replace(" ", "\ ")) cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) @@ -195,13 +198,13 @@ class AbstractSolver(object): if cmd[0] == "\"": # Command line executable path is between " char cmd = cmd.split("\"") - exe = cmd[1] + exe = cmd[1].replace("\ ", " ") args = "\"".join(cmd[2:]).split(" ")[1:] else: # We suppose the command line executable path as no space char cmd = cmd.replace("\ ", "&_&").split(" ") - exe = cmd[0].replace("&_&", "\ ") - args = cmd[1:] + exe = cmd[0].replace("&_&", " ") + args = list(map(lambda s: s.replace("&_&", "\ "), cmd[1:])) logger.info(f"! {exe} {args}") return exe, args @@ -212,7 +215,7 @@ class AbstractSolver(object): return True cmd = self._cmd_input - exe, args = self._format_command(cmd) + exe, args = self._format_command(cmd, self._path_input) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -231,7 +234,7 @@ class AbstractSolver(object): return True cmd = self._cmd_solver - exe, args = self._format_command(cmd) + exe, args = self._format_command(cmd, self._path_solver) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -251,7 +254,7 @@ class AbstractSolver(object): return True cmd = self._cmd_output - exe, args = self._format_command(cmd) + exe, args = self._format_command(cmd, self._path_output) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" From d78310476ec44d52b252008208772c4899102b3d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 8 Aug 2023 13:48:01 +0200 Subject: [PATCH 134/289] Results: Add results model, view scheme and mage results reading. --- src/Model/Saved.py | 2 +- src/Solver/ASolver.py | 14 ++++ src/Solver/Mage.py | 139 +++++++++++++++++++++++++++++++++++ src/View/RunSolver/Window.py | 15 +++- src/View/ui/SolverLog.ui | 6 ++ 5 files changed, 174 insertions(+), 2 deletions(-) diff --git a/src/Model/Saved.py b/src/Model/Saved.py index a1b91fd5..60013cde 100644 --- a/src/Model/Saved.py +++ b/src/Model/Saved.py @@ -33,5 +33,5 @@ class SavedStatus(object): self._saved = True def modified(self): - logger.debug("model status set as modified") + # logger.debug("model status set as modified") self._saved = False diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index 300bb36c..64cc01cb 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -19,6 +19,8 @@ import os import logging +from tools import timer + try: from signal import SIGTERM, SIGSTOP, SIGCONT _signal = True @@ -29,6 +31,9 @@ from enum import Enum from Model.Except import NotImplementedMethodeError +from Model.Results.Results import Results +from Model.Results.River.River import River, Reach, Profile + logger = logging.getLogger() class STATUS(Enum): @@ -164,6 +169,15 @@ class AbstractSolver(object): """ raise NotImplementedMethodeError(self, self.log_file) + ########### + # RESULTS # + ########### + + @timer + def results(self, study, repertory, qlog = None): + results = Results(study = study) + return results + ####### # Run # ####### diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 0d43d2ef..907f5ff3 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -17,12 +17,19 @@ # -*- coding: utf-8 -*- import os +import logging +import numpy as np from tools import timer from Solver.ASolver import AbstractSolver from Checker.Mage import MageNetworkGraphChecker +from Model.Results.Results import Results +from Model.Results.River.River import River, Reach, Profile + +logger = logging.getLogger() + def mage_file_open(filepath, mode): f = open(filepath, mode) @@ -337,6 +344,21 @@ class Mage(AbstractSolver): return True + ########### + # RESULTS # + ########### + + def read_bin(self, study, repertory, results, qlog = None): + return + + @timer + def results(self, study, repertory, qlog = None): + results = Results(study = study) + + self.read_bin(study, repertory, results, qlog) + + return results + ########## # MAGE 7 # ########## @@ -450,3 +472,120 @@ class Mage8(Mage): self._export_REP(study, repertory, files, qlog) return True + + ########### + # RESULTS # + ########### + + def read_bin(self, study, repertory, results, qlog = None): + with mage_file_open(os.path.join(repertory, f"0.BIN"), "r") as f: + newline = lambda: np.fromfile(f, dtype=np.int32, count=1) + endline = lambda: 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) + + # Meta data (1st line) + newline() + + data = read_int(3) + + nb_reach = data[0] + nb_profile = data[1] + mage_version = data[2] + + logger.debug(f"read_bin: nb_reach = {nb_reach}") + logger.debug(f"read_bin: nb_profile = {nb_profile}") + logger.debug(f"read_bin: mage_version = {mage_version}") + + if mage_version <= 80: + msg = ( + "Read BIN files: " + + f"Possible incompatible mage version '{mage_version}', " + + "please check your solver configuration..." + ) + logger.warning(msg) + + if qlog is not None: + qlog.put("[WARNING] " + msg) + + results.set("solver_version", f"Mage8 ({mage_version})") + results.set("nb_reach", f"{nb_reach}") + results.set("nb_profile", f"{nb_profile}") + + endline() + + # Reach information (2nd line) + newline() + + reachs = [] + iprofiles = {} + reach_offset = {} + + ip_to_r = lambda i: iprofiles[ + next( + filter( + lambda k: k[0] <= i <= k[1], + iprofiles + ) + ) + ] + ip_to_ri = lambda r, i: i - reach_offset[r] + + data = read_int(2*nb_reach) + + for i in range(nb_reach): + # Add results reach to reach list + r = results.river.add(i) + reachs.append(r) + + # ID of first and last reach profiles + i1 = data[2*i] - 1 + i2 = data[2*i+1] - 1 + + # Add profile id correspondance to reach + key = (i1, i2) + iprofiles[key] = r + + # Profile ID offset + reach_offset[r] = i1 + + logger.debug(f"read_bin: iprofiles = {iprofiles}") + + endline() + + # X (3rd line) + newline() + _ = read_float(nb_profile) + endline() + + # Z and Y (4th line) + newline() + _ = read_float(3*nb_profile) + endline() + + # Data + newline() + + 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() + data = read_float(n) + + logger.debug(f"read_bin: timestamp = {timestamp} sec") + for i, d in enumerate(data): + # Get reach corresponding to profile ID + reach = ip_to_r(i) + # Get profile id in reach + ri = ip_to_ri(reach, i) + + # Set data for profile RI + reach.set(ri, timestamp, key, d) + + endline() + end = newline().size <= 0 + + logger.info(reachs[0].profiles[0]._data) diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 4f915950..b03a4603 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -16,8 +16,9 @@ # -*- coding: utf-8 -*- -import tempfile import os +import logging +import tempfile from queue import Queue from tools import trace, timer @@ -53,6 +54,8 @@ except: _translate = QCoreApplication.translate +logger = logging.getLogger() + class SelectSolverWindow(ASubWindow, ListedSubWindow): def __init__(self, title="Select solver", study=None, config=None, @@ -110,6 +113,8 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self._config = config self._solver = solver + self._results = None + super(SolverLogWindow, self).__init__( name=self._title, ui="SolverLog", parent=parent ) @@ -169,6 +174,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_pause").triggered.connect(self.pause) self.find(QAction, "action_stop").triggered.connect(self.stop) self.find(QAction, "action_log_file").triggered.connect(self.log_file) + self.find(QAction, "action_results").triggered.connect(self.results) self._alarm.timeout.connect(self.update) @@ -193,6 +199,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_start").setEnabled(True) self.find(QAction, "action_pause").setEnabled(False) self.find(QAction, "action_stop").setEnabled(False) + self.find(QAction, "action_results").setEnabled(True) if self._solver.log_file() != "": self.find(QAction, "action_log_file").setEnabled(True) @@ -221,6 +228,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_pause").setEnabled(False) self.find(QAction, "action_stop").setEnabled(True) self.find(QAction, "action_log_file").setEnabled(False) + self.find(QAction, "action_results").setEnabled(False) def pause(self): self._log(" *** Pause", color="blue") @@ -229,6 +237,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_start").setEnabled(True) self.find(QAction, "action_pause").setEnabled(False) self.find(QAction, "action_stop").setEnabled(True) + self.find(QAction, "action_results").setEnabled(False) def stop(self): self._log(" *** Stop", color="blue") @@ -237,9 +246,13 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_start").setEnabled(True) self.find(QAction, "action_pause").setEnabled(False) self.find(QAction, "action_stop").setEnabled(False) + self.find(QAction, "action_results").setEnabled(True) if self._solver.log_file() != "": self.find(QAction, "action_log_file").setEnabled(True) + def results(self): + self._results = self._solver.results(self._study, self._workdir, qlog = self._output) + def log_file(self): file_name = os.path.join(self._workdir, self._solver.log_file()) log = SolverLogFileWindow( diff --git a/src/View/ui/SolverLog.ui b/src/View/ui/SolverLog.ui index bbbc3edb..c94c57db 100644 --- a/src/View/ui/SolverLog.ui +++ b/src/View/ui/SolverLog.ui @@ -63,6 +63,7 @@ + @@ -100,6 +101,11 @@ LogFile + + + results + + From 0c750ec2447d5965aaa2ee4388b6f0b7c00ca620 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 8 Aug 2023 16:17:15 +0200 Subject: [PATCH 135/289] Results: Minor change. --- src/Solver/Mage.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 907f5ff3..cc695556 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -478,6 +478,8 @@ class Mage8(Mage): ########### def read_bin(self, study, repertory, results, qlog = None): + logger.info(f"read_bin: Start ...") + with mage_file_open(os.path.join(repertory, f"0.BIN"), "r") as f: newline = lambda: np.fromfile(f, dtype=np.int32, count=1) endline = lambda: np.fromfile(f, dtype=np.int32, count=1) @@ -523,16 +525,6 @@ class Mage8(Mage): iprofiles = {} reach_offset = {} - ip_to_r = lambda i: iprofiles[ - next( - filter( - lambda k: k[0] <= i <= k[1], - iprofiles - ) - ) - ] - ip_to_ri = lambda r, i: i - reach_offset[r] - data = read_int(2*nb_reach) for i in range(nb_reach): @@ -568,6 +560,17 @@ class Mage8(Mage): # Data newline() + ip_to_r = lambda i: iprofiles[ + next( + filter( + lambda k: k[0] <= i <= k[1], + iprofiles + ) + ) + ] + ip_to_ri = lambda r, i: i - reach_offset[r] + + ts = set() end = False while not end: n = read_int(1)[0] @@ -576,16 +579,20 @@ class Mage8(Mage): data = read_float(n) logger.debug(f"read_bin: timestamp = {timestamp} sec") - for i, d in enumerate(data): - # Get reach corresponding to profile ID - reach = ip_to_r(i) - # Get profile id in reach - ri = ip_to_ri(reach, i) + ts.add(timestamp) - # Set data for profile RI - reach.set(ri, timestamp, key, d) + if key in ["Z", "Q"]: + for i, d in enumerate(data): + # Get reach corresponding to profile ID + reach = ip_to_r(i) + # Get profile id in reach + ri = ip_to_ri(reach, i) + + # Set data for profile RI + reach.set(ri, timestamp, key, d) endline() end = newline().size <= 0 - logger.info(reachs[0].profiles[0]._data) + logger.debug(reachs[0].profiles[0]._data) + logger.info(f"read_bin: ... end with {len(ts)} timestamp read") From 1f0d49bd4def2ea9cc512898377255a0fd15c388 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 8 Aug 2023 16:36:28 +0200 Subject: [PATCH 136/289] Results: Add results model. --- src/Model/Results/Results.py | 45 ++++++++++++++++++ src/Model/Results/River/River.py | 81 ++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/Model/Results/Results.py create mode 100644 src/Model/Results/River/River.py diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py new file mode 100644 index 00000000..b3a83e43 --- /dev/null +++ b/src/Model/Results/Results.py @@ -0,0 +1,45 @@ +# Results.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 . + +import logging +import numpy as np + +from copy import deepcopy +from datetime import datetime + +from Model.Results.River.River import River + +logger = logging.getLogger() + +class Results(object): + def __init__(self, study = None): + self._study = study + self._river = River(self._study) + + self._meta_data = { + # Keep results creation date + "creation_date": datetime.now(), + } + + @property + def river(self): + return self._river + + def set(self, key, value): + self._meta_data[key] = value + + def get(self, key): + return self._meta_data[key] diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py new file mode 100644 index 00000000..7425f532 --- /dev/null +++ b/src/Model/Results/River/River.py @@ -0,0 +1,81 @@ +# River.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 . + +import logging + +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 {: {: , ...}, ...} + + def set(self, timestamp, key, data): + if timestamp not in self._data: + self._data[timestamp] = {} + + self._data[timestamp][key] = data + + def get_ts(self, timestamp): + return self._data[timestamp] + + def get_key(self, timestamp): + return list( + map(lambda ts: self._data[ts][key], self._data) + ) + + 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._profiles = list( + map( + lambda p: Profile(p, self._study), + reach.profiles + ) + ) + + @property + def profiles(self): + return self._profiles.copy() + + 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 + + # Dict with timestamps as key + self._reachs = [] + + @property + def reachs(self): + return self.reachs.copy() + + def add(self, reach_id): + reachs = self._study.river.enable_edges() + + new = Reach(reachs[reach_id].reach, self._study) + + self._reachs.append(new) + return new From 6f95e514b93f39f0ba880150f406d82cc54309b5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 8 Aug 2023 16:36:59 +0200 Subject: [PATCH 137/289] Results: Add results view file scheme and ui. --- src/View/Results/PlotAC.py | 17 ++++ src/View/Results/PlotKPC.py | 17 ++++ src/View/Results/PlotXY.py | 17 ++++ src/View/Results/Table.py | 17 ++++ src/View/Results/UndoCommand.py | 17 ++++ src/View/Results/Window.py | 174 ++++++++++++++++++++++++++++++++ src/View/Results/translate.py | 17 ++++ src/View/ui/Results.ui | 96 ++++++++++++++++++ 8 files changed, 372 insertions(+) create mode 100644 src/View/Results/PlotAC.py create mode 100644 src/View/Results/PlotKPC.py create mode 100644 src/View/Results/PlotXY.py create mode 100644 src/View/Results/Table.py create mode 100644 src/View/Results/UndoCommand.py create mode 100644 src/View/Results/Window.py create mode 100644 src/View/Results/translate.py create mode 100644 src/View/ui/Results.ui diff --git a/src/View/Results/PlotAC.py b/src/View/Results/PlotAC.py new file mode 100644 index 00000000..7c882e6e --- /dev/null +++ b/src/View/Results/PlotAC.py @@ -0,0 +1,17 @@ +# PlotAC.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py new file mode 100644 index 00000000..c044d35e --- /dev/null +++ b/src/View/Results/PlotKPC.py @@ -0,0 +1,17 @@ +# PlotKPC.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py new file mode 100644 index 00000000..a8c4b206 --- /dev/null +++ b/src/View/Results/PlotXY.py @@ -0,0 +1,17 @@ +# PlotXY.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 -*- diff --git a/src/View/Results/Table.py b/src/View/Results/Table.py new file mode 100644 index 00000000..bfe2dbaa --- /dev/null +++ b/src/View/Results/Table.py @@ -0,0 +1,17 @@ +# Table.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- diff --git a/src/View/Results/UndoCommand.py b/src/View/Results/UndoCommand.py new file mode 100644 index 00000000..ca18efaa --- /dev/null +++ b/src/View/Results/UndoCommand.py @@ -0,0 +1,17 @@ +# UndoCommand.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py new file mode 100644 index 00000000..90955df5 --- /dev/null +++ b/src/View/Results/Window.py @@ -0,0 +1,174 @@ +# 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 PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, +) + +from View.Plot.MplCanvas import MplCanvas +from View.Geometry.PlotKPC import PlotKPC + +from View.Results.Table import TableModel +from View.Results.translate import * +from View.Stricklers.Window import StricklersWindow + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class ResultsWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="Results", + study=None, solver=None, results=None, + parent=None): + self._study = study + self._solver = solver + self._results = results + + self.setup_title(title) + + super(ResultsWindow, self).__init__( + name=title, ui="Results", 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._solver.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): + 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, + undo = self._undo_stack, + ) + table.setModel(self._table[t]) + + 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) + self.canvas.setObjectName("canvas") + self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.canvas) + + # self.plot = PlotKPC( + # canvas = self.canvas, + # data = self._reach.reach, + # toolbar = None, + # display_current = False + # ) + # self.plot.draw() + + self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_2.setObjectName("canvas_2") + self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout_2.addWidget(self.canvas_2) + + # self.plot_2 = PlotStricklers( + # canvas = self.canvas_2, + # data = self._reach, + # toolbar = None + # ) + # self.plot_2.draw() + + self.canvas_3 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_3.setObjectName("canvas_3") + self.plot_layout_3 = self.find(QVBoxLayout, "verticalLayout_3") + self.plot_layout_3.addWidget(self.canvas_3) + + + 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, + } + + for t in ["reach", "profile"]: + table = self.find(QTableView, f"tableView_{t}") + + table.selectionModel()\ + .selectionChanged\ + .connect(fun[t]) + + self._table.dataChanged.connect(fun[t]) + + def _set_current_reach(self): + return + + def _set_current_profile(self): + return + + def copy(self): + logger.info("TODO: copy") + + def paste(self): + logger.info("TODO: paste") + + def undo(self): + self._table.undo() + + def redo(self): + self._table.redo() diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py new file mode 100644 index 00000000..dad67d85 --- /dev/null +++ b/src/View/Results/translate.py @@ -0,0 +1,17 @@ +# 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 -*- diff --git a/src/View/ui/Results.ui b/src/View/ui/Results.ui new file mode 100644 index 00000000..2f886339 --- /dev/null +++ b/src/View/ui/Results.ui @@ -0,0 +1,96 @@ + + + MainWindow + + + + 0 + 0 + 942 + 655 + + + + MainWindow + + + + + + + + + + Qt::Horizontal + + + + Qt::Vertical + + + + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + true + + + true + + + + + + + Qt::Vertical + + + + Qt::Horizontal + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 942 + 22 + + + + + + + + From b31be5ac36b673d49eec2cf7cc57cea4698ebac5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 08:05:39 +0200 Subject: [PATCH 138/289] pamhyr: Change PAMHYR ascii art to PAMHYR2. --- src/pamhyr.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pamhyr.py b/src/pamhyr.py index 1c7ec5ca..e67d4e82 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -61,11 +61,12 @@ def license(): blue("""`7MM\"""Mq. db `7MMM. ,MMF'`7MMF' `7MMF'`YMM' `MM'`7MM\"""Mq.""") blue(""" MM `MM. ;MM: MMMb dPMM MM MM VMA ,V MM `MM.""") - blue(""" MM ,M9 ,V^MM. M YM ,M MM MM MM VMA ,V MM ,M9""") - blue(""" MMmmdM9 ,M `MM M Mb M' MM MMmmmmmmMM VMMP MMmmdM9""") - blue(""" MM AbmmmqMA M YM.P' MM MM MM MM MM YM.""") - blue(""" MM A' VML M `YM' MM MM MM MM MM `Mb.""") - blue(""".JMML. .AMA. .AMMA..JML. `' .JMML..JMML. .JMML. .JMML. .JMML. .JMM.""") + blue(""" MM ,M9 ,V^MM. M YM ,M MM MM MM VMA ,V MM ,M9 pd*"*b.""") + blue(""" MMmmdM9 ,M `MM M Mb M' MM MMmmmmmmMM VMMP MMmmdM9 (O) j8""") + blue(""" MM AbmmmqMA M YM.P' MM MM MM MM MM YM. ,;j9""") + blue(""" MM A' VML M `YM' MM MM MM MM MM `Mb. ,-='""") + blue(""".JMML. .AMA. .AMMA..JML. `' .JMML..JMML. .JMML. .JMML. .JMML. .JMM.Ammmmmmm""") + with open(os.path.abspath( os.path.join( os.path.dirname(__file__), From d499636cb674602fb4c0964ef2bf8c3a2ca09333 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 08:45:44 +0200 Subject: [PATCH 139/289] Results: Add tables and open window. --- src/Model/Results/Results.py | 5 ++ src/Model/Results/River/River.py | 20 ++++++- src/View/MainWindow.py | 33 ++++++++++++ src/View/Results/Table.py | 90 ++++++++++++++++++++++++++++++++ src/View/Results/Window.py | 5 +- src/View/Results/translate.py | 13 +++++ src/View/RunSolver/Window.py | 5 +- 7 files changed, 167 insertions(+), 4 deletions(-) diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index b3a83e43..67698f4e 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -34,6 +34,11 @@ class Results(object): "creation_date": datetime.now(), } + @property + def date(self): + date = self._meta_data["creation_date"] + return f"{date.isoformat(sep=' ')}" + @property def river(self): return self._river diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 7425f532..22312a9a 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -26,6 +26,14 @@ class Profile(object): self._profile = profile # Source profile in the study self._data = {} # Dict of dict {: {: , ...}, ...} + @property + def name(self): + return self._profile.name + + @property + def kp(self): + return self._profile.kp + def set(self, timestamp, key, data): if timestamp not in self._data: self._data[timestamp] = {} @@ -54,10 +62,17 @@ class Reach(object): ) ) + @property + def name(self): + return self._reach.name + @property def profiles(self): return self._profiles.copy() + def profile(self, id): + return self._profiles[id] + def set(self, profile_id, timestamp, key, data): self._profiles[profile_id].set(timestamp, key, data) @@ -70,7 +85,10 @@ class River(object): @property def reachs(self): - return self.reachs.copy() + return self._reachs.copy() + + def reach(self, id): + return self._reachs[id] def add(self, reach_id): reachs = self._study.river.enable_edges() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index d5508a10..559601c2 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -52,6 +52,7 @@ from View.Frictions.Window import FrictionsWindow 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 @@ -105,6 +106,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): # Model self.model = None + # Results + self._last_results = None + # UI self.ui = loadUi( os.path.join(os.path.dirname(__file__), "ui", "MainWindow.ui"), @@ -304,6 +308,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): for action in model_action: self.enable_actions(action, not no_model) + def set_results(self, results): + self._last_results = results + ############ # FEATURES # ############ @@ -638,6 +645,32 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) sol.show() + def open_solver_results(self, solver, results = None): + # If no specific results, get last results + if results is None: + results = self._last_results + + # No results available + if results is None: + return + + # Windows already opened + res = self.sub_win_filter_first( + "Results", + contain = [solver.name, results.date] + ) + + if res is None: + res = ResultsWindow( + study = self.model, + solver = solver, + results = results, + parent = self + ) + res.show() + else: + res.activateWindow() + ######### # DEBUG # ######### diff --git a/src/View/Results/Table.py b/src/View/Results/Table.py index bfe2dbaa..8e167459 100644 --- a/src/View/Results/Table.py +++ b/src/View/Results/Table.py @@ -15,3 +15,93 @@ # along with this program. If not, see . # -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import timer, trace + +from PyQt5.QtGui import ( + QKeySequence, QColor +) +from PyQt5.QtCore import ( + Qt, QAbstractTableModel, QModelIndex, + QVariant, pyqtSlot, QCoreApplication, +) +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, + QStyledItemDelegate, QLineEdit, QAbstractItemView, + QComboBox, +) + +from View.Results.translate import * + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class TableModel(QAbstractTableModel): + def __init__(self, results = None, study = None, mode = None, undo=None): + super(QAbstractTableModel, self).__init__() + + 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 data(self, index, role=Qt.DisplayRole): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._mode == "reach": + if self._headers[column] == "name": + v = self._results.river.reach(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 + return str(v) + elif self._headers[column] == "kp": + v = current_reach.profile(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 diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 90955df5..e4c1b65c 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -75,7 +75,8 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._title = ( title + " - " + self._study.name + " - " - + self._solver.name + + self._solver.name + " - " + + self._results.date ) def setup_sc(self): @@ -153,7 +154,7 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): .selectionChanged\ .connect(fun[t]) - self._table.dataChanged.connect(fun[t]) + self._table[t].dataChanged.connect(fun[t]) def _set_current_reach(self): return diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py index dad67d85..4b5ab3c5 100644 --- a/src/View/Results/translate.py +++ b/src/View/Results/translate.py @@ -15,3 +15,16 @@ # along with this program. If not, see . # -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + +table_headers_reach = { + "name": _translate("Results", "Reach name"), +} + +table_headers_profile = { + "name": _translate("Results", "Name"), + "kp": _translate("Results", "KP (m)"), +} diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index b03a4603..872d72ef 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -219,6 +219,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self._process = self.new_process(self._parent) self._log(" *** Start", color="blue") + self._results = None self._solver.start(self._process) self.find(QAction, "action_start").setEnabled(False) @@ -251,7 +252,9 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_log_file").setEnabled(True) def results(self): - self._results = self._solver.results(self._study, self._workdir, qlog = self._output) + if self._results is None: + self._results = self._solver.results(self._study, self._workdir, qlog = self._output) + self._parent.open_solver_results(self._solver, self._results) def log_file(self): file_name = os.path.join(self._workdir, self._solver.log_file()) From 2faab43aed65332040ab53666bac0c5c47f95c65 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 10:36:57 +0200 Subject: [PATCH 140/289] Results: Add XY plot (without water zone) --- src/Model/Results/River/River.py | 8 ++ src/View/Results/PlotXY.py | 121 +++++++++++++++++++++++++++++++ src/View/Results/Window.py | 18 +++-- 3 files changed, 139 insertions(+), 8 deletions(-) diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 22312a9a..ae8b33c0 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -34,6 +34,10 @@ class Profile(object): def kp(self): return self._profile.kp + @property + def geometry(self): + return self._profile + def set(self, timestamp, key, data): if timestamp not in self._data: self._data[timestamp] = {} @@ -66,6 +70,10 @@ class Reach(object): def name(self): return self._reach.name + @property + def geometry(self): + return self._reach + @property def profiles(self): return self._profiles.copy() diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index a8c4b206..43934073 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -15,3 +15,124 @@ # along with this program. If not, see . # -*- coding: utf-8 -*- + +from tools import timer, trace +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +class PlotXY(APlot): + def __init__(self, canvas=None, results=None, + reach_id=0, profile_id=0, + toolbar=None, display_current=True): + super(PlotXY, self).__init__( + canvas=canvas, + data=results, + toolbar=toolbar + ) + + self.display_current = display_current + + self.line_xy = [] + self.line_gl = [] + + self._current_reach_id = reach_id + self._current_profile_id = profile_id + + @property + def results(self): + return self.data + + @timer + def draw(self, highlight=None): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.results is None: + return + + reach = self.results.river.reach(self._current_reach_id) + + if reach.geometry.number_profiles == 0: + self._init = False + return + + kp_min, kp_max = (-1, -1) + if highlight is not None: + kp_min, kp_max = highlight + + # Axes + self.canvas.axes.set_xlabel( + _translate("Results", "X (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("Results", "Y (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.axis("equal") + + kp = reach.geometry.get_kp() + self.canvas.axes.set_xlim( + left = min(kp), right = max(kp) + ) + + # Draw line for each profile + self.line_xy = [ + self.canvas.axes.plot( + x, y, lw=1., + color='b' if kp_min <= kp <= kp_max else 'r', + markersize=3, marker='+' + ) + for x, y, kp in zip( + reach.geometry.get_x(), + reach.geometry.get_y(), + kp + ) + ] + + # Guide lines + x_complete = reach.geometry.get_guidelines_x() + y_complete = reach.geometry.get_guidelines_y() + + self.line_gl = [ + self.canvas.axes.plot( + x, y, + ) + for x, y in zip(x_complete, y_complete) + ] + + if self.display_current: + # Current profile + profile = reach.geometry.profile(self._current_profile_id) + + self.plot_selected, = self.canvas.axes.plot( + profile.x(), + profile.y(), + lw=1., markersize=3, + marker='+', color="b" + ) + self.plot_selected.set_visible(False) + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.axes.autoscale() + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + self.draw() + + def update(self): + self.draw() diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index e4c1b65c..e2d3f8b3 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -40,8 +40,8 @@ from PyQt5.QtWidgets import ( ) from View.Plot.MplCanvas import MplCanvas -from View.Geometry.PlotKPC import PlotKPC +from View.Results.PlotXY import PlotXY from View.Results.Table import TableModel from View.Results.translate import * from View.Stricklers.Window import StricklersWindow @@ -110,13 +110,15 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) - # self.plot = PlotKPC( - # canvas = self.canvas, - # data = self._reach.reach, - # toolbar = None, - # display_current = False - # ) - # self.plot.draw() + self.plot_xy = PlotXY( + canvas = self.canvas, + results = self._results, + reach_id = 0, + profile_id = 0, + toolbar = None, + display_current = False + ) + self.plot_xy.draw() self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) self.canvas_2.setObjectName("canvas_2") From b04e325ab5d2a4fb4394fb1c55445e8e53dd16f8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 11:00:07 +0200 Subject: [PATCH 141/289] Results: PlotXY: Add water level. --- src/Model/Results/River/River.py | 2 +- src/Solver/Mage.py | 1 + src/View/Results/PlotXY.py | 32 +++++++++++++++++++++++++++++++- src/View/Results/Window.py | 11 +++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index ae8b33c0..5ae1e90f 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -47,7 +47,7 @@ class Profile(object): def get_ts(self, timestamp): return self._data[timestamp] - def get_key(self, timestamp): + def get_key(self, key): return list( map(lambda ts: self._data[ts][key], self._data) ) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index cc695556..6c129f88 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -595,4 +595,5 @@ class Mage8(Mage): end = newline().size <= 0 logger.debug(reachs[0].profiles[0]._data) + results.set("timestamps", ts) logger.info(f"read_bin: ... end with {len(ts)} timestamp read") diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index 43934073..a0af6a2d 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +from functools import reduce + from tools import timer, trace from View.Plot.APlot import APlot @@ -40,6 +42,7 @@ class PlotXY(APlot): self.line_xy = [] self.line_gl = [] + self._current_timestamp = max(results.get("timestamps")) self._current_reach_id = reach_id self._current_profile_id = profile_id @@ -106,9 +109,32 @@ class PlotXY(APlot): for x, y in zip(x_complete, y_complete) ] + # Display point under water + for profile in reach.profiles: + water_z = profile.get_ts_key( + self._current_timestamp, "Z" + ) + pgeo = profile.geometry + + x, y = reduce( + lambda acc, pts: (acc[0] + [pts.x], acc[1] + [pts.y]), + filter( + lambda pts: pts.z < water_z, + pgeo.points + ), + ([], []) + ) + + self.canvas.axes.plot( + x, y, lw=1., + color='b', + markersize=4, + marker='o' + ) + if self.display_current: # Current profile - profile = reach.geometry.profile(self._current_profile_id) + profile = reach.profile(self._current_profile_id).geometry self.plot_selected, = self.canvas.axes.plot( profile.x(), @@ -134,5 +160,9 @@ class PlotXY(APlot): self._current_profile_id = profile_id self.draw() + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.draw() + def update(self): self.draw() diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index e2d3f8b3..66845431 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -158,6 +158,17 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._table[t].dataChanged.connect(fun[t]) + def plotXY(self, reach_id = None, profile_id = None, timestamp = None): + if reach_id is not None: + self.plot_xy.set_reach(reach_id) + if profile_id is not None: + self.plot_xy.set_profile(profile_id) + if timestamp is not None: + self.plot_xy.set_timestamp(timestamp) + + self.plot_xy.draw() + + def _set_current_reach(self): return From 05411c468daba087ef29fa09e2ef702332bb9c71 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 11:54:52 +0200 Subject: [PATCH 142/289] Results: Connect sliders. --- src/View/Results/Window.py | 36 ++++++++++++++++++++++++++++++++++-- src/View/ui/Results.ui | 6 +++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 66845431..dbf335f0 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -37,6 +37,7 @@ from PyQt5.QtWidgets import ( QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, QComboBox, QVBoxLayout, QHeaderView, QTabWidget, + QSlider, ) from View.Plot.MplCanvas import MplCanvas @@ -58,6 +59,8 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._solver = solver self._results = results + self._timestamps = sorted(list(self._results.get("timestamps"))) + self.setup_title(title) super(ResultsWindow, self).__init__( @@ -67,6 +70,7 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.setup_sc() self.setup_table() self.setup_graph() + self.setup_slider() self.setup_connections() self.ui.setWindowTitle(self._title) @@ -104,6 +108,14 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) + def setup_slider(self): + self._slider_profile = self.find(QSlider, f"verticalSlider_profile") + default_reach = self._results.river.reach(0) + self._slider_profile.setMaximum(len(default_reach.profiles) - 1) + + self._slider_time = self.find(QSlider, f"horizontalSlider_time") + self._slider_time.setMaximum(len(self._timestamps) - 1) + def setup_graph(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") @@ -158,7 +170,11 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._table[t].dataChanged.connect(fun[t]) - def plotXY(self, reach_id = None, profile_id = None, timestamp = None): + self._slider_profile.valueChanged.connect(self._set_current_profile_slider) + self._slider_time.valueChanged.connect(self._set_current_timestamp) + + + def update(self, reach_id = None, profile_id = None, timestamp = None): if reach_id is not None: self.plot_xy.set_reach(reach_id) if profile_id is not None: @@ -170,11 +186,27 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): def _set_current_reach(self): - return + table = self.find(QTableView, f"tableView_reach") + indexes = table.selectedIndexes() + + self.update(reach_id = indexes[0]) def _set_current_profile(self): + table = self.find(QTableView, f"tableView_profile") + indexes = table.selectedIndexes() + + self.update(profile_id = indexes[0]) + + def _set_current_profile_slider(self): + pid = self._slider_profile.value() + self.update(profile_id = pid) + return + def _set_current_timestamp(self): + timestamp = self._timestamps[self._slider_time.value()] + self.update(timestamp = timestamp) + def copy(self): logger.info("TODO: copy") diff --git a/src/View/ui/Results.ui b/src/View/ui/Results.ui index 2f886339..64cde207 100644 --- a/src/View/ui/Results.ui +++ b/src/View/ui/Results.ui @@ -30,17 +30,17 @@ - + - + Qt::Horizontal - + Qt::Vertical From 2ba3c16a9876fb510e1b3ef35ce7f3d768177c71 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 15:40:20 +0200 Subject: [PATCH 143/289] Results: Add the 2 last plot into results view. --- src/View/InitialConditions/PlotDKP.py | 2 +- src/View/Results/PlotAC.py | 98 +++++++++++++++++++++++++++ src/View/Results/PlotKPC.py | 95 ++++++++++++++++++++++++++ src/View/Results/PlotXY.py | 2 +- src/View/Results/Window.py | 41 ++++++++--- 5 files changed, 228 insertions(+), 10 deletions(-) diff --git a/src/View/InitialConditions/PlotDKP.py b/src/View/InitialConditions/PlotDKP.py index 23752aef..58a83219 100644 --- a/src/View/InitialConditions/PlotDKP.py +++ b/src/View/InitialConditions/PlotDKP.py @@ -42,7 +42,7 @@ class PlotDKP(APlot): return self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Draft (m)"), + _translate("MainWindow_reach", "Elevation (m)"), color='green', fontsize=11 ) self.canvas.axes.set_xlabel( diff --git a/src/View/Results/PlotAC.py b/src/View/Results/PlotAC.py index 7c882e6e..f0e4b0e2 100644 --- a/src/View/Results/PlotAC.py +++ b/src/View/Results/PlotAC.py @@ -15,3 +15,101 @@ # along with this program. If not, see . # -*- coding: utf-8 -*- + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +class PlotAC(APlot): + def __init__(self, canvas=None, results=None, + reach_id=0, profile_id=0, + toolbar=None): + super(PlotAC, self).__init__( + canvas=canvas, + data=results, + toolbar=toolbar + ) + + self._current_timestamp = max(results.get("timestamps")) + self._current_reach_id = reach_id + self._current_profile_id = profile_id + + @property + def results(self): + return self.data + + @timer + def draw(self, highlight=None): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.results is None: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='green', fontsize=11 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Elevation (m)"), + color='green', fontsize=11 + ) + + reach = self.results.river.reach(self._current_reach_id) + profile = reach.profile(self._current_profile_id) + x = profile.geometry.get_station() + z = profile.geometry.z() + + self.canvas.axes.set_xlim( + left = min(x), right = max(x) + ) + + self.line_kp, = self.canvas.axes.plot( + x, z, + linestyle="solid", + lw=1.8, + color='grey', + ) + + kp = reach.geometry.get_kp() + + # Water elevation + water_z = profile.get_ts_key(self._current_timestamp, "Z") + + self.canvas.axes.plot( + [min(x), max(x)], [water_z, water_z], + lw=1., color='b', + ) + + x_z = list(map(lambda _: water_z, x)) + self.canvas.axes.fill_between( + x, z, water_z, + where=z <= water_z, + color='blue', alpha=0.5, interpolate=True + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + self.draw() + + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.draw() + + def update(self): + self.draw() diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py index c044d35e..4d7f69a6 100644 --- a/src/View/Results/PlotKPC.py +++ b/src/View/Results/PlotKPC.py @@ -15,3 +15,98 @@ # along with this program. If not, see . # -*- coding: utf-8 -*- + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +class PlotKPC(APlot): + def __init__(self, canvas=None, results=None, + reach_id=0, profile_id=0, + toolbar=None): + super(PlotKPC, self).__init__( + canvas=canvas, + data=results, + toolbar=toolbar + ) + + self._current_timestamp = max(results.get("timestamps")) + self._current_reach_id = reach_id + self._current_profile_id = profile_id + + @property + def results(self): + return self.data + + @timer + def draw(self, highlight=None): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.results is None: + return + + reach = self.results.river.reach(self._current_reach_id) + + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Elevation (m)"), + color='green', fontsize=11 + ) + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "KP (m)"), + color='green', fontsize=11 + ) + + kp = reach.geometry.get_kp() + z_min = reach.geometry.get_z_min() + + self.canvas.axes.set_xlim( + left = min(kp), right = max(kp) + ) + + self.line_kp_zmin = self.canvas.axes.plot( + kp, z_min, + color='grey', lw=1. + ) + + if len(reach.geometry.profiles) != 0: + kp = reach.geometry.get_kp() + + # Water elevation + water_z = list( + map( + lambda p: p.get_ts_key(self._current_timestamp, "Z"), + reach.profiles + ) + ) + + self.canvas.axes.plot( + kp, water_z, lw=1., + color='b', + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + self.draw() + + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.draw() + + def update(self): + self.draw() diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index a0af6a2d..905692a3 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -88,7 +88,7 @@ class PlotXY(APlot): self.line_xy = [ self.canvas.axes.plot( x, y, lw=1., - color='b' if kp_min <= kp <= kp_max else 'r', + color='b' if kp_min <= kp <= kp_max else 'grey', markersize=3, marker='+' ) for x, y, kp in zip( diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index dbf335f0..4ae5293e 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -43,6 +43,9 @@ from PyQt5.QtWidgets import ( from View.Plot.MplCanvas import MplCanvas from View.Results.PlotXY import PlotXY +from View.Results.PlotAC import PlotAC +from View.Results.PlotKPC import PlotKPC + from View.Results.Table import TableModel from View.Results.translate import * from View.Stricklers.Window import StricklersWindow @@ -112,9 +115,11 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._slider_profile = self.find(QSlider, f"verticalSlider_profile") default_reach = self._results.river.reach(0) self._slider_profile.setMaximum(len(default_reach.profiles) - 1) + self._slider_profile.setValue(0) self._slider_time = self.find(QSlider, f"horizontalSlider_time") self._slider_time.setMaximum(len(self._timestamps) - 1) + self._slider_time.setValue(len(self._timestamps) - 1) def setup_graph(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) @@ -137,18 +142,28 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") self.plot_layout_2.addWidget(self.canvas_2) - # self.plot_2 = PlotStricklers( - # canvas = self.canvas_2, - # data = self._reach, - # toolbar = None - # ) - # self.plot_2.draw() + self.plot_kpc = PlotKPC( + canvas = self.canvas_2, + results = self._results, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_kpc.draw() self.canvas_3 = MplCanvas(width=5, height=4, dpi=100) self.canvas_3.setObjectName("canvas_3") self.plot_layout_3 = self.find(QVBoxLayout, "verticalLayout_3") 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 = None + ) + self.plot_ac.draw() def setup_connections(self): self.undo_sc.activated.connect(self.undo) @@ -177,25 +192,35 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): 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) + self.plot_kpc.set_reach(reach_id) if profile_id is not None: self.plot_xy.set_profile(profile_id) + self.plot_ac.set_profile(profile_id) + self.plot_kpc.set_profile(profile_id) if timestamp is not None: self.plot_xy.set_timestamp(timestamp) + self.plot_ac.set_timestamp(timestamp) + self.plot_kpc.set_timestamp(timestamp) self.plot_xy.draw() + self.plot_ac.draw() + self.plot_kpc.draw() def _set_current_reach(self): table = self.find(QTableView, f"tableView_reach") indexes = table.selectedIndexes() - self.update(reach_id = indexes[0]) + self.update(reach_id = indexes[0].row()) def _set_current_profile(self): table = self.find(QTableView, f"tableView_profile") indexes = table.selectedIndexes() - self.update(profile_id = indexes[0]) + ind = indexes[0].row() + self.update(profile_id = ind) + self._slider_profile.setValue(ind) def _set_current_profile_slider(self): pid = self._slider_profile.value() From bf80d870d397d9c52b5ffe5a414a2e150565cece Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 15:40:57 +0200 Subject: [PATCH 144/289] pamhyr: Minor change. --- src/pamhyr.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pamhyr.py b/src/pamhyr.py index e67d4e82..55172c78 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -61,11 +61,11 @@ def license(): blue("""`7MM\"""Mq. db `7MMM. ,MMF'`7MMF' `7MMF'`YMM' `MM'`7MM\"""Mq.""") blue(""" MM `MM. ;MM: MMMb dPMM MM MM VMA ,V MM `MM.""") - blue(""" MM ,M9 ,V^MM. M YM ,M MM MM MM VMA ,V MM ,M9 pd*"*b.""") - blue(""" MMmmdM9 ,M `MM M Mb M' MM MMmmmmmmMM VMMP MMmmdM9 (O) j8""") - blue(""" MM AbmmmqMA M YM.P' MM MM MM MM MM YM. ,;j9""") - blue(""" MM A' VML M `YM' MM MM MM MM MM `Mb. ,-='""") - blue(""".JMML. .AMA. .AMMA..JML. `' .JMML..JMML. .JMML. .JMML. .JMML. .JMM.Ammmmmmm""") + blue(""" MM ,M9 ,V^MM. M YM ,M MM MM MM VMA ,V MM ,M9 pd*"*b.""") + blue(""" MMmmdM9 ,M `MM M Mb M' MM MMmmmmmmMM VMMP MMmmdM9 (O) j8""") + blue(""" MM AbmmmqMA M YM.P' MM MM MM MM MM YM. ,;j9""") + blue(""" MM A' VML M `YM' MM MM MM MM MM `Mb. ,-='""") + blue(""".JMML. .AMA. .AMMA..JML. `' .JMML..JMML. .JMML. .JMML. .JMML. .JMM. Ammmmmmm""") with open(os.path.abspath( os.path.join( From 24bc3738277a7c69a9de1cbd280927a4c0d88bea Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 15:41:48 +0200 Subject: [PATCH 145/289] trans: Update (incomplete). --- src/lang/fr.ts | 246 +++++++++++++++++++++++++++++-------------------- 1 file changed, 144 insertions(+), 102 deletions(-) diff --git a/src/lang/fr.ts b/src/lang/fr.ts index e3581eb9..9bc3f345 100644 --- a/src/lang/fr.ts +++ b/src/lang/fr.ts @@ -1,6 +1,5 @@ - - + About @@ -12,7 +11,7 @@ BoundaryCondition - + Not associate Non associer @@ -93,9 +92,14 @@ - Discharge (m³/s) + Discharge (m³/s) Débit (m³/s) + + + Discharge (m³/s) + + Checker @@ -502,7 +506,7 @@ Frictions - + Not defined Non définie @@ -535,27 +539,27 @@ Geometry - + Name Nom - + Kp (m) Pk (m) - + Type Type - + upstream amont - + downstream aval @@ -583,7 +587,7 @@ Commentaire - + Not associate Non associer @@ -679,9 +683,14 @@ - Discharge (m³/s) + Discharge (m³/s) Débit (m³/s) + + + Discharge (m³/s) + + MainWindow @@ -1301,22 +1310,22 @@ Ressayer la vérification - + Stop Stopper - + Start Commencer - + Pause Pause - + LogFile Fichier de log @@ -1385,41 +1394,46 @@ Define initial conditions Définire les conditions initiales - + Édition des Tronçons - + Ouvrir une étude - + Enrégistrer étude en cours (Ctrl+S) - + Fermer étude en cours (Ctrl+F) + + + results + + - + MainWindowProfile - + Profile - + (no name) - + Quittez ? @@ -1439,34 +1453,34 @@ - + Nom - + Abs en travers (m) - + La cote du fond Z minimale - + La cote maximale Z maximale - + Rive gauche - + Rive droite @@ -1480,102 +1494,102 @@ Cote (m) - - + + Suppression les lignes incomplètes - - + + Supprimer les lignes des cellules non renseignées ? - - + + Suppression des noms répétés - - + + Etes-vous sûr de vouloir quitter ? - + Insérer un point - + Supprimer le/les point(s) sélectionnés - + Trier les points par ordre décroissant de X - + Trier les points par ordre décroissant de Y - + Décaler le point sélectionné vers le haut - + Décaler le point sélectionné vers le bas - + Exporter (dans un fichier) les points du profil au format tabulé - + Copier la sélection au format tabulé - + Coller la sélection depuis le presse-papier au format tabulé - + Vérifier la validité de la saisie et garder ou pas les modifications apportées - + Annuler toutes les modifications depuis la dernière validation - + Annuler toutes les modifications et revenir à l'état initial - + Ligne d'eau : - Z : Cote (m) - A : Aire mouillée (mu00B2) - p : Périmètre mouillé (m) - L : Largeur au miroir (m) + Z : Cote (m) + A : Aire mouillée (mu00B2) + p : Périmètre mouillé (m) + L : Largeur au miroir (m) - + 'Maj + Clic' : Ligne d'eau & 'Ctrl + Clic' : Sélectionner des points - - + + Abscisse en travers calculée en projétant les points sur le plan défini par les deux points nommés extrêmes @@ -1594,37 +1608,37 @@ sur le plan défini par les deux points nommés extrêmes Pk (m) - + Ouvrir un fichier - + Fichiers .ST (*.ST) - + Fichiers .M (*.M) - + Tous les fichiers (*) - + Kp : - + Files .ST(*.ST or *.st) - + All files (*) @@ -1719,11 +1733,6 @@ Ordre des sections : Amont --> Aval Y (m) - - - Draft (m) - - KP (m) @@ -1734,141 +1743,174 @@ Ordre des sections : Amont --> Aval Discharge (m^3/s) Débit (m³/s) - + Choisissez un nom de fichier à sauvegarder - - + + Édition des profils sélectionnés - - + + Vous avez sélectionné plus de 5 profils. Seuls les 5 premiers seront édités. - - + + Profil N° : - + Profil précédent - + Profil sélectionné - + Importer une géométrie - + Supprimer le profil sélectionné - + Éditer le profil sélectionné - + Copier le profil sélectionné - + Coller le profil en fin de liste (penser à modifier le Pk avant de trier) - + Dupliquer la section sélectionnée - + Trier les profils par ordre décroissant des Pk - + Changer l'ordre des profils (en décalant le profil sélectionné vers le haut) - + Changer l'ordre des profils (en décalant le profil sélectionné vers le bas) - + Terminer l'édition - + Vue isométrique (Alt+Z) - + Vue isométrique (Alt+E) - + Vue isométrique (Alt+R) + + + Elevation (m) + Altitude (m) + Network - + Add node Ajouter un nœud - + Delete the node Supprimer un nœud - + Disable the node Déactiver un nœud - + Delete the reach Supprimer un bief - + Disable the reach Déactiver un bief - + Enable the reach Activer un bief - + Reverse the reach orientation Inverser l'orientation du bief + + Results + + + X (m) + + + + + Y (m) + + + + + Reach name + + + + + Name + Nom + + + + KP (m) + + + SolverParameters @@ -2072,17 +2114,17 @@ Seuls les 5 premiers seront édités. Enregistrer la figure - + Retour à la vue précédente - + Passer à la vue suivante - + Vue isométrique (Shift+W) From 6356bab3207fb60bad517eeff17f0bf3051becaa Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 17:21:59 +0200 Subject: [PATCH 146/289] Results: Plot: Fill between in KPC and fill water zone in XY. --- src/View/Results/PlotKPC.py | 5 +++++ src/View/Results/PlotXY.py | 43 ++++++++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py index 4d7f69a6..68f6fb4e 100644 --- a/src/View/Results/PlotKPC.py +++ b/src/View/Results/PlotKPC.py @@ -90,6 +90,11 @@ class PlotKPC(APlot): color='b', ) + self.canvas.axes.fill_between( + kp, z_min, water_z, + color='blue', alpha=0.5, interpolate=True + ) + self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() if self.toolbar is not None: diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index 905692a3..a88071f8 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +import logging + from functools import reduce from tools import timer, trace @@ -27,6 +29,8 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate +logger = logging.getLogger() + class PlotXY(APlot): def __init__(self, canvas=None, results=None, reach_id=0, profile_id=0, @@ -109,7 +113,23 @@ class PlotXY(APlot): for x, y in zip(x_complete, y_complete) ] + if self.display_current: + # Current profile + profile = reach.profile(self._current_profile_id).geometry + + self.plot_selected, = self.canvas.axes.plot( + profile.x(), + profile.y(), + lw=1., markersize=3, + marker='+', color="b" + ) + self.plot_selected.set_visible(False) + # Display point under water + poly_l_x = [] + poly_l_y = [] + poly_r_x = [] + poly_r_y = [] for profile in reach.profiles: water_z = profile.get_ts_key( self._current_timestamp, "Z" @@ -125,24 +145,23 @@ class PlotXY(APlot): ([], []) ) + poly_l_x.append(x[0]) + poly_l_y.append(y[0]) + poly_r_x.append(x[-1]) + poly_r_y.append(y[-1]) + self.canvas.axes.plot( x, y, lw=1., color='b', - markersize=4, + markersize=1, marker='o' ) - if self.display_current: - # Current profile - profile = reach.profile(self._current_profile_id).geometry - - self.plot_selected, = self.canvas.axes.plot( - profile.x(), - profile.y(), - lw=1., markersize=3, - marker='+', color="b" - ) - self.plot_selected.set_visible(False) + poly_x = poly_l_x + list(reversed(poly_r_x)) + [poly_l_x[0]] + poly_y = poly_l_y + list(reversed(poly_r_y)) + [poly_l_y[0]] + # FIXME: Fill do not works ? (tmp fix: use plot) + self.canvas.axes.fill(poly_x, poly_y, color='blue', alpha=0.5) + self.canvas.axes.plot(poly_x, poly_y, color='blue', alpha=0.5) self.canvas.axes.autoscale_view(True, True, True) self.canvas.axes.autoscale() From 32fb90521fbada3d37764264b6b9a4d89a482da0 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 17:32:19 +0200 Subject: [PATCH 147/289] Results: Plot: XY: Fix fill. --- src/View/Results/PlotXY.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index a88071f8..3e7a88f3 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -159,9 +159,8 @@ class PlotXY(APlot): poly_x = poly_l_x + list(reversed(poly_r_x)) + [poly_l_x[0]] poly_y = poly_l_y + list(reversed(poly_r_y)) + [poly_l_y[0]] - # FIXME: Fill do not works ? (tmp fix: use plot) - self.canvas.axes.fill(poly_x, poly_y, color='blue', alpha=0.5) - self.canvas.axes.plot(poly_x, poly_y, color='blue', alpha=0.5) + + self.canvas.axes.fill(poly_x, poly_y, color='blue', alpha=1) self.canvas.axes.autoscale_view(True, True, True) self.canvas.axes.autoscale() From add8261cebd63dcd13baa691dd0981c184fc8dc7 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 9 Aug 2023 17:47:20 +0200 Subject: [PATCH 148/289] Results: Fix minor bug and add tab widget to result window. --- src/View/Results/Window.py | 4 +++ src/View/ui/Results.ui | 72 +++++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 4ae5293e..99022b2f 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -211,12 +211,16 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): def _set_current_reach(self): table = self.find(QTableView, f"tableView_reach") indexes = table.selectedIndexes() + if len(indexes) == 0: + return self.update(reach_id = indexes[0].row()) def _set_current_profile(self): table = self.find(QTableView, f"tableView_profile") indexes = table.selectedIndexes() + if len(indexes) == 0: + return ind = indexes[0].row() self.update(profile_id = ind) diff --git a/src/View/ui/Results.ui b/src/View/ui/Results.ui index 64cde207..d975641b 100644 --- a/src/View/ui/Results.ui +++ b/src/View/ui/Results.ui @@ -17,7 +17,7 @@ - + @@ -30,13 +30,51 @@ - - - - - - Qt::Horizontal + + + + + + 0 + + + Geometry + + + + + + Qt::Vertical + + + + Qt::Horizontal + + + + + + + + + + + + + + + + + + Hydrograph + + + + + + + @@ -52,25 +90,11 @@ - - + + - Qt::Vertical + Qt::Horizontal - - - Qt::Horizontal - - - - - - - - - - - From 35ee6617ac6aaa41764530aa09229a966ccc4cef Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 11 Aug 2023 09:28:15 +0200 Subject: [PATCH 149/289] Results: Add hydrograph. --- src/View/Results/PlotH.py | 131 +++++++++++++++++++++++++++++++++++++ src/View/Results/Window.py | 19 ++++++ 2 files changed, 150 insertions(+) create mode 100644 src/View/Results/PlotH.py diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py new file mode 100644 index 00000000..dcc9f7f6 --- /dev/null +++ b/src/View/Results/PlotH.py @@ -0,0 +1,131 @@ +# PlotH.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 functools import reduce + +from tools import timer, trace +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + +class PlotH(APlot): + def __init__(self, canvas=None, results=None, + reach_id=0, profile_id=0, + toolbar=None, display_current=True): + super(PlotH, self).__init__( + canvas=canvas, + data=results, + toolbar=toolbar + ) + + self.display_current = display_current + + self._current_timestamp = max(results.get("timestamps")) + self._current_reach_id = reach_id + self._current_profile_id = profile_id + + @property + def results(self): + return self.data + + @timer + def draw(self, highlight=None): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.results is None: + return + + reach = self.results.river.reach(self._current_reach_id) + profile = reach.profile(self._current_profile_id) + + if reach.geometry.number_profiles == 0: + self._init = False + return + + kp_min, kp_max = (-1, -1) + if highlight is not None: + kp_min, kp_max = highlight + + # Axes + self.canvas.axes.set_xlabel( + _translate("Results", "Time (s)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("Results", "Discharge (m³/s)"), + color='green', fontsize=12 + ) + + ts = list(self.results.get("timestamps")) + ts.sort() + + self.canvas.axes.set_xlim( + left = min(ts), right = max(ts) + ) + + # Draw discharge for each timestamp + x = ts + y = profile.get_key("Q") + + if len(ts) != len(x): + logger.warning( + "Results as less Q data ({len(x)}) " + + "than timestamps ({len(ts)}) " + + "for profile {self._current_profile_id}" + ) + return + + self._line = [ + self.canvas.axes.plot( + x, y, lw=1., + color='r', + markersize=3, marker='+' + ) + ] + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.axes.autoscale() + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + self.draw() + + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.draw() + + def update(self): + self.draw() diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 99022b2f..22d0a2c4 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -45,6 +45,7 @@ from View.Plot.MplCanvas import MplCanvas from View.Results.PlotXY import PlotXY from View.Results.PlotAC import PlotAC from View.Results.PlotKPC import PlotKPC +from View.Results.PlotH import PlotH from View.Results.Table import TableModel from View.Results.translate import * @@ -165,6 +166,20 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): ) self.plot_ac.draw() + self.canvas_4 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_4.setObjectName("canvas_4") + self.plot_layout_4 = self.find(QVBoxLayout, "verticalLayout_hydrograph") + 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 = None + ) + self.plot_h.draw() + def setup_connections(self): self.undo_sc.activated.connect(self.undo) self.redo_sc.activated.connect(self.redo) @@ -194,18 +209,22 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_xy.set_reach(reach_id) self.plot_ac.set_reach(reach_id) self.plot_kpc.set_reach(reach_id) + self.plot_h.set_reach(reach_id) if profile_id is not None: self.plot_xy.set_profile(profile_id) self.plot_ac.set_profile(profile_id) self.plot_kpc.set_profile(profile_id) + self.plot_h.set_profile(profile_id) if timestamp is not None: self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) self.plot_kpc.set_timestamp(timestamp) + self.plot_h.set_timestamp(timestamp) self.plot_xy.draw() self.plot_ac.draw() self.plot_kpc.draw() + self.plot_h.draw() def _set_current_reach(self): From bb56c9293823fa335cab70fd4683070c575fd436 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 11 Aug 2023 09:57:08 +0200 Subject: [PATCH 150/289] BC, LC: Fix axes scale for none time data. --- src/View/BoundaryCondition/Edit/Plot.py | 7 +++++++ src/View/LateralContribution/Edit/Plot.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index c9e36413..b27380a5 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +import logging + from datetime import datetime from tools import timer, trace @@ -29,6 +31,8 @@ from View.BoundaryCondition.Edit.translate import * _translate = QCoreApplication.translate +logger = logging.getLogger() + class Plot(APlot): def __init__(self, canvas=None, data=None, mode = "time", toolbar=None): @@ -41,6 +45,9 @@ class Plot(APlot): self._mode = mode def custom_ticks(self): + if self.data.header[0] != "time": + return + t0 = datetime.fromtimestamp(0) nb = len(self.data.data) mod = int(nb / 5) diff --git a/src/View/LateralContribution/Edit/Plot.py b/src/View/LateralContribution/Edit/Plot.py index df088405..5843ee56 100644 --- a/src/View/LateralContribution/Edit/Plot.py +++ b/src/View/LateralContribution/Edit/Plot.py @@ -41,6 +41,9 @@ class Plot(APlot): self._mode = mode def custom_ticks(self): + if self.data.header[0] != "time": + return + t0 = datetime.fromtimestamp(0) nb = len(self.data.data) mod = int(nb / 5) From 768a0b9f14ee6d865b9e4811b05d92f02489c63f Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 11 Aug 2023 15:46:09 +0200 Subject: [PATCH 151/289] Sediment: Add solid BC, QSO export and profile + point sediment layers export. --- .../BoundaryCondition/BoundaryCondition.py | 5 +- .../BoundaryConditionList.py | 3 +- .../BoundaryConditionTypes.py | 13 +++ src/Solver/ASolver.py | 14 +++- src/Solver/Mage.py | 76 ++++++++++++++++-- src/View/BoundaryCondition/Edit/translate.py | 3 +- src/View/BoundaryCondition/translate.py | 7 +- tests_cases/Saar/Saar.pamhyr | Bin 176128 -> 192512 bytes 8 files changed, 107 insertions(+), 14 deletions(-) diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index 86e0f1d2..d6abb6c9 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -83,7 +83,8 @@ class BoundaryCondition(SQLSubModel): def _get_ctor_from_type(cls, t): from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, - TimeOverZ, TimeOverDischarge, ZOverDischarge + TimeOverZ, TimeOverDischarge, ZOverDischarge, + Solid, ) res = NotDefined @@ -95,6 +96,8 @@ class BoundaryCondition(SQLSubModel): res = TimeOverDischarge elif t == "ZD": res = ZOverDischarge + elif t == "SL": + res = Solid return res @classmethod diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py index 1a342dc7..2514f326 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionList.py +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -26,7 +26,8 @@ from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, - TimeOverZ, TimeOverDischarge, ZOverDischarge + TimeOverZ, TimeOverDischarge, ZOverDischarge, + Solid, ) class BoundaryConditionList(SQLSubModel): diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py index b2c73f11..963e88e7 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -83,3 +83,16 @@ class ZOverDischarge(BoundaryCondition): @property def _default_0(self): return 0.0 + + +class Solid(BoundaryCondition): + def __init__(self, id:int = -1, name:str = "", status = None): + super(Solid, self).__init__(id=id, name=name, status=status) + + self._type = "SL" + self._header = ["time", "solid"] + self._types = [TimeOverDischarge.time_convert, float] + + @classmethod + def compatibility(cls): + return ["solid"] diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index 64cc01cb..ea268999 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -213,12 +213,22 @@ class AbstractSolver(object): # Command line executable path is between " char cmd = cmd.split("\"") exe = cmd[1].replace("\ ", " ") - args = "\"".join(cmd[2:]).split(" ")[1:] + args = list( + filter( + lambda s: s != "", + "\"".join(cmd[2:]).split(" ")[1:] + ) + ) else: # We suppose the command line executable path as no space char cmd = cmd.replace("\ ", "&_&").split(" ") exe = cmd[0].replace("&_&", " ") - args = list(map(lambda s: s.replace("&_&", "\ "), cmd[1:])) + args = list( + filter( + lambda s: s != "", + map(lambda s: s.replace("&_&", "\ "), cmd[1:]) + ) + ) logger.info(f"! {exe} {args}") return exe, args diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 6c129f88..81f99722 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -127,23 +127,47 @@ class Mage(AbstractSolver): with mage_file_open(os.path.join(repertory, f"{name}.ST"), "w+") as f: files.append(f"{name}.ST") + cnt_num = 1 for profile in edge.reach.profiles: - num = f"{profile.num:>6}" + num = f"{cnt_num:>6}" c1 = f"{profile.code1:>6}" c2 = f"{profile.code2:>6}" t = f"{len(profile.points):>6}" kp = f"{profile.kp:>13.4f}" - name = profile.name + pname = profile.name + if profile.name == "": + pname = f"p{profile.id:>3}".replace(" ", "p") + name = f"{pname}" - f.write(f"{num}{c1}{c2}{t}{kp} {name}\n") + sediment = "" + if profile.sl is not None: + if not any(filter(lambda f: ".GRA" in f, files)): + files.append("0.GRA") + + nl = len(profile.sl) + sediment = f" {nl:>3}" + for l in profile.sl.layers: + sediment += f" {l.height:>10} {l.d50:>10} {l.sigma:>10} {l.critical_constraint:>10}" + + f.write(f"{num}{c1}{c2}{t}{kp} {name} {sediment}\n") + cnt_num += 1 for point in profile.points: x = f"{point.x:>13.4f}" y = f"{point.y:>13.4f}" z = f"{point.z:>13.4f}" - n = point.name + n = f"{point.name:<3}" - f.write(f"{x}{y}{z} {n}\n") + sediment = "" + prev = point.z + if point.sl is not None: + nl = len(point.sl) + sediment = f"{nl:>3}" + for l in point.sl.layers: + prev = round(prev - l.height, 5) + sediment += f" {prev:>10} {l.d50:>10} {l.sigma:>10} {l.critical_constraint:>10}" + + f.write(f"{x}{y}{z} {n} {sediment}\n") f.write(f" 999.9990 999.9990 999.9990\n") @@ -182,6 +206,7 @@ class Mage(AbstractSolver): AVA = [] HYD = [] LIM = [] + QSO = [] for tab in ["liquid", "solid", "suspenssion"]: for bound in lst.get_tab(tab): @@ -191,10 +216,13 @@ class Mage(AbstractSolver): HYD.append(bound) elif bound.bctype == "TZ": LIM.append(bound) + elif bound.bctype == "SL": + QSO.append(bound) files = files + self._export_BC("AVA", AVA, repertory, qlog) files = files + self._export_BC("HYD", HYD, repertory, qlog) files = files + self._export_BC("LIM", LIM, repertory, qlog) + files = files + self._export_QSO(QSO, repertory, qlog) return files @@ -332,12 +360,20 @@ class Mage(AbstractSolver): for file in files: EXT = file.split('.')[1] - f.write(f"{EXT} {file}\n") + if EXT not in ["ST", "GRA"]: + f.write(f"{EXT} {file}\n") f.write("* OUTPUT\n") f.write(f"TRA 0.TRA\n") f.write(f"BIN 0.BIN\n") + for file in files: + EXT = file.split('.')[1] + + if EXT in ["GRA"]: + f.write(f"{EXT} {file}\n") + + @timer def export(self, study, repertory, qlog = None): self._export_ST(study, repertory, qlog) @@ -459,11 +495,37 @@ class Mage8(Mage): return files + @timer + def _export_QSO(self, bounds, repertory, qlog): + files = [] + + if len(bounds) == 0: + return files + + if qlog is not None: + qlog.put(f"Export QSO file") + + with mage_file_open(os.path.join(repertory, f"0.QSO"), "w+") as f: + files.append(f"0.QSO") + + for bound in bounds: + name = f"{bound.node.id:3}".replace(" ", "x") + f.write(f"* {bound.node.name} ({name}) {bound.bctype}\n") + f.write(f"${name}\n") + header = bound.header + f.write(f"*{header[0]:>9}|{header[1]:>10}\n") + + for d in bound.data: + f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n") + + return files + + @timer def export(self, study, repertory, qlog = None): files = [] - self._export_ST(study, repertory, qlog) + files = self._export_ST(study, repertory, qlog) files = files + self._export_PAR(study, repertory, qlog) files = files + self._export_NET(study, repertory, qlog) files = files + self._export_bound_cond(study, repertory, qlog) diff --git a/src/View/BoundaryCondition/Edit/translate.py b/src/View/BoundaryCondition/Edit/translate.py index 4c09f5fc..c8039795 100644 --- a/src/View/BoundaryCondition/Edit/translate.py +++ b/src/View/BoundaryCondition/Edit/translate.py @@ -26,5 +26,6 @@ table_headers = { "time": _translate("BoundaryCondition", "Time"), "date": _translate("BoundaryCondition", "Date"), "discharge": _translate("BoundaryCondition", "Discharge (m³/s)"), - "z": _translate("BoundaryCondition", "Z (m)") + "z": _translate("BoundaryCondition", "Z (m)"), + "solid": _translate("BoundaryCondition", "Solid (kg/s)"), } diff --git a/src/View/BoundaryCondition/translate.py b/src/View/BoundaryCondition/translate.py index 661c1ad0..c4ff8f88 100644 --- a/src/View/BoundaryCondition/translate.py +++ b/src/View/BoundaryCondition/translate.py @@ -20,7 +20,8 @@ from PyQt5.QtCore import QCoreApplication from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, - TimeOverZ, TimeOverDischarge, ZOverDischarge + TimeOverZ, TimeOverDischarge, ZOverDischarge, + Solid ) _translate = QCoreApplication.translate @@ -31,6 +32,7 @@ long_types = { "TZ": _translate("BoundaryCondition", "Time over Z"), "TD": _translate("BoundaryCondition", "Time over Discharge"), "ZD": _translate("BoundaryCondition", "Z over Discharge"), + "SL": _translate("BoundaryCondition", "Solid"), } table_headers = { @@ -44,5 +46,6 @@ BC_types = { "PC": PonctualContribution, "TZ": TimeOverZ, "TD": TimeOverDischarge, - "ZD": ZOverDischarge + "ZD": ZOverDischarge, + "SL": Solid, } diff --git a/tests_cases/Saar/Saar.pamhyr b/tests_cases/Saar/Saar.pamhyr index df56a2984353c1c2f9d8f73ffc277b4fab764ba6..711ef7bc929574e1ca841b76442e1262bddd4c8b 100644 GIT binary patch literal 192512 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 literal 176128 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 b1?E~{t_9{=V6Fw`T41gP=33zY*B1D1^QhA{ From cd07f3aa8db6cdec7050a4b03ec62c15608e7696 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 21 Aug 2023 10:55:27 +0200 Subject: [PATCH 152/289] Sediment: Add SL to geometry into DB and minor change. --- src/Model/Geometry/PointXYZ.py | 4 +++- src/Model/Geometry/ProfileXYZ.py | 4 +++- src/Model/SolverParameters/SolverParametersList.py | 7 +++++++ src/Model/Study.py | 2 +- src/Solver/Mage.py | 4 ++-- src/View/SolverParameters/translate.py | 4 ++-- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 33a3bebf..19695c77 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -44,7 +44,9 @@ class PointXYZ(Point, SQLSubModel): y INTEGER NOT NULL, z INTEGER NOT NULL, profile INTEGER NOT NULL, - FOREIGN KEY(profile) REFERENCES profileXYZ(id) + sl INTEGER, + FOREIGN KEY(profile) REFERENCES profileXYZ(id), + FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) ) """) diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 12c4ea77..e818eabc 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -76,7 +76,9 @@ class ProfileXYZ(Profile, SQLSubModel): num INTEGER NOT NULL, code1 INTEGER NOT NULL, code2 INTEGER NOT NULL, - FOREIGN KEY(reach) REFERENCES river_reach(id) + sl INTEGER, + FOREIGN KEY(reach) REFERENCES river_reach(id), + FOREIGN KEY(sl) REFERENCES sedimentary_layer(id) ) """) diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index a6471356..ce62ff7c 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -93,6 +93,13 @@ class SolverParametersList(SQLSubModel): @classmethod def _sql_update(cls, execute, version): + major, minor, release = version.strip().split(".") + + 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'") + return cls._update_submodel(execute, version) @classmethod diff --git a/src/Model/Study.py b/src/Model/Study.py index c907da3e..5b44d0a7 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -40,7 +40,7 @@ class Study(SQLModel): def __init__(self, filename = None, init_new = True): # Metadata - self._version = "0.0.2" + self._version = "0.0.3" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 81f99722..77cc83d3 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -61,11 +61,11 @@ class Mage(AbstractSolver): ("mage_timestep_tra", "3600"), ("mage_timestep_bin", "0"), # ("mage_timestep_melissa", "0"), - ("mage_implication", "0.70"), + ("mage_implicitation", "0.70"), ("mage_continuity_discretization", "S"), ("mage_qsj_discretization", "B"), ("mage_stop_criterion_iterations", "R"), - ("mage_iter_type", "0"), + ("mage_iteration_type", "0"), ("mage_smooth_coef", "0"), ("mage_cfl_max", "-1."), ("mage_min_height", "0.1"), diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 76cb70ac..3d07e6b3 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -59,11 +59,11 @@ def init(): "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_implication": _translate("SolverParameters", "Implicitation parameter"), + "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_iter_type": _translate("SolverParameters", "Iteration type"), + "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)"), From d58a509c1d3def03f17758145da52355e3a79154 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 21 Aug 2023 11:37:01 +0200 Subject: [PATCH 153/289] Solver: Add custom command line parameters and solver default command line args. --- src/Model/River.py | 15 ++++++ .../SolverParameters/SolverParametersList.py | 11 ++++ src/Solver/ASolver.py | 51 ++++++++++++------- src/Solver/Mage.py | 16 ++++++ src/View/RunSolver/Window.py | 3 +- src/View/SolverParameters/translate.py | 1 + 6 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/Model/River.py b/src/Model/River.py index 3041c6c7..91d765ba 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +from tools import flatten + from Model.DB import SQLSubModel from Model.Network.Node import Node @@ -382,3 +384,16 @@ class River(Graph, SQLSubModel): def set_current_reach(self, reach): self._current_reach = reach + + def has_sediment(self): + has = len(self._sediment_layers) != 0 + has &= any( + filter( + lambda p: p.sl != None, + flatten( + map(lambda e: e.reach.profiles, self.edges()) + ) + ) + ) + + return has diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index ce62ff7c..18c45529 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -167,6 +167,17 @@ class SolverParametersList(SQLSubModel): def get(self, index): return self._parameters[index] + def get_by_key(self, key): + try: + return next( + filter( + lambda p: p["name"] == key, + self._parameters + ) + )["value"] + except: + return None + def set(self, index, new): self._parameters[index] = new self._status.modified() diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index ea268999..186abbbb 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -86,6 +86,7 @@ class AbstractSolver(object): ("all_init_time", "000:00:00:00"), ("all_final_time", "999:99:00:00"), ("all_timestep", "300.0"), + ("all_command_line_arguments", ""), ] return lst @@ -153,6 +154,17 @@ class AbstractSolver(object): def export(self, study, repertory, qlog = None): raise NotImplementedMethodeError(self, self.export) + def cmd_args(self, study): + """Return solver command line arguments list + + Returns: + Command line arguments list + """ + params = study.river.get_params(self.type) + args = params.get_by_key("all_command_line_arguments") + + return args.split(" ") + def input_param(self): """Return input command line parameter(s) @@ -190,7 +202,7 @@ class AbstractSolver(object): ) ) - def _format_command(self, cmd, path = ""): + def _format_command(self, study, cmd, path = ""): """Format command line Args: @@ -206,6 +218,7 @@ class AbstractSolver(object): cmd = cmd.replace("@path", path.replace(" ", "\ ")) cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) + cmd = cmd.replace("@args", " ".join(self.cmd_args(study))) logger.debug(f"! {cmd}") @@ -233,13 +246,13 @@ class AbstractSolver(object): logger.info(f"! {exe} {args}") return exe, args - def run_input_data_fomater(self): + def run_input_data_fomater(self, study): if self._cmd_input == "": - self._run_next() + self._run_next(study) return True cmd = self._cmd_input - exe, args = self._format_command(cmd, self._path_input) + exe, args = self._format_command(study, cmd, self._path_input) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -252,13 +265,13 @@ class AbstractSolver(object): return True - def run_solver(self): + def run_solver(self, study): if self._cmd_solver == "": - self._run_next() + self._run_next(study) return True cmd = self._cmd_solver - exe, args = self._format_command(cmd, self._path_solver) + exe, args = self._format_command(study, cmd, self._path_solver) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -272,13 +285,13 @@ class AbstractSolver(object): self._status = STATUS.RUNNING return True - def run_output_data_fomater(self): + def run_output_data_fomater(self, study): if self._cmd_output == "": - self._run_next() + self._run_next(study) return True cmd = self._cmd_output - exe, args = self._format_command(cmd, self._path_output) + exe, args = self._format_command(study, cmd, self._path_output) if not os.path.exists(exe): error = f"[ERROR] Path {exe} do not exists" @@ -297,29 +310,29 @@ class AbstractSolver(object): for x in s.split('\n'): self._output.put(x) - def _run_next(self): + def _run_next(self, study): self._step += 1 if self._step < len(self._runs): - res = self._runs[self._step]() + res = self._runs[self._step](study) if res is not True: self._output.put(res) else: self._status = STATUS.STOPED - def _finished(self, exit_code, exit_status): + def _finished(self, study, exit_code, exit_status): if self._output is not None: self._output.put(exit_code) - self._run_next() + self._run_next(study) - def run(self, 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(self._finished) + self._process.finished.connect(lambda c, s: self._finished(study, c, s)) self._runs = [ self.run_input_data_fomater, @@ -328,7 +341,7 @@ class AbstractSolver(object): ] self._step = 0 # Run first step - res = self._runs[0]() + res = self._runs[0](study) if res is not True: self._output.put(res) @@ -340,14 +353,14 @@ class AbstractSolver(object): self._status = STATUS.STOPED return True - def start(self, process = None): + def start(self, study, process = None): if _signal: if self._status == STATUS.PAUSED: os.kill(self._process.pid(), SIGCONT) self._status = STATUS.RUNNING return True - self.run(process) + self.run(study, process) return True def pause(self): diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 77cc83d3..5e7909d7 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -97,6 +97,14 @@ class Mage(AbstractSolver): # Export # ########## + def cmd_args(self, study): + l = super(Mage, self).cmd_args(study) + + l.append("-r") + l.append("-fp=1") + + return l + def input_param(self): return "0.REP" @@ -440,6 +448,14 @@ class Mage8(Mage): # Export # ########## + def cmd_args(self, study): + l = super(Mage8, self).cmd_args(study) + + if study.river.has_sediment(): + l.append("-c=3") + + return l + @timer def _export_PAR(self, study, repertory, qlog = None): files = [] diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 872d72ef..c307157a 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -147,6 +147,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self._log(f" *** Run solver {self._solver.name}", color="blue") self._solver.run( + study, process = self._process, output_queue = self._output ) @@ -220,7 +221,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self._log(" *** Start", color="blue") self._results = None - self._solver.start(self._process) + self._solver.start(self._study, process = self._process) self.find(QAction, "action_start").setEnabled(False) if _signal: diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 3d07e6b3..2508629e 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -55,6 +55,7 @@ def init(): "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"), From 596cd24cc33977197181b11017b3674896b35603 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 21 Aug 2023 16:18:28 +0200 Subject: [PATCH 154/289] Sediment: Add GRA file reading. --- src/Model/Results/River/River.py | 3 + src/Solver/Mage.py | 162 ++++++++++++++++++++++++++++++- 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 5ae1e90f..9f72f731 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -98,6 +98,9 @@ class River(object): def reach(self, id): return self._reachs[id] + def has_reach(self, id): + return 0 <= id < len(self._reachs) + def add(self, reach_id): reachs = self._study.river.enable_edges() diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 5e7909d7..9bc9331e 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -101,7 +101,6 @@ class Mage(AbstractSolver): l = super(Mage, self).cmd_args(study) l.append("-r") - l.append("-fp=1") return l @@ -475,7 +474,6 @@ class Mage8(Mage): f.write(f"{name} {value}\n") - return files @timer @@ -555,6 +553,7 @@ class Mage8(Mage): # RESULTS # ########### + @timer def read_bin(self, study, repertory, results, qlog = None): logger.info(f"read_bin: Start ...") @@ -675,3 +674,162 @@ class Mage8(Mage): logger.debug(reachs[0].profiles[0]._data) results.set("timestamps", ts) logger.info(f"read_bin: ... end with {len(ts)} timestamp read") + + @timer + def read_gra(self, study, repertory, results, qlog = None): + # if not study.river.has_sediment(): + # return + + logger.info(f"read_gra: Start ...") + + with mage_file_open(os.path.join(repertory, f"0.GRA"), "r") as f: + newline = lambda: np.fromfile(f, dtype=np.int32, count=1) + endline = lambda: 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) + + # Meta data (1st line) + newline() + + data = read_int(3) + + nb_reach = data[0] + nb_profile = data[1] + mage_version = data[2] + + logger.debug(f"read_gra: nb_reach = {nb_reach}") + logger.debug(f"read_gra: nb_profile = {nb_profile}") + logger.debug(f"read_gra: mage_version = {mage_version}") + + if mage_version <= 80: + msg = ( + "Read GRA files: " + + f"Possible incompatible mage version '{mage_version}', " + + "please check your solver configuration..." + ) + logger.warning(msg) + + if qlog is not None: + qlog.put("[WARNING] " + msg) + + results.set("gra_solver_version", f"Mage8 ({mage_version})") + results.set("gra_nb_reach", f"{nb_reach}") + results.set("gra_nb_profile", f"{nb_profile}") + + endline() + + # Reach information (2nd line) + newline() + + reachs = [] + iprofiles = {} + reach_offset = {} + + data = read_int(2*nb_reach) + + for i in range(nb_reach): + # Get results reach to reach list + r = results.river.reach(i) + reachs.append(r) + + # ID of first and last reach profiles + i1 = data[2*i] - 1 + i2 = data[2*i+1] - 1 + + # Add profile id correspondance to reach + key = (i1, i2) + iprofiles[key] = r + + # Profile ID offset + reach_offset[r] = i1 + + logger.debug(f"read_gra: iprofiles = {iprofiles}") + + endline() + + # X (3rd line) + newline() + _ = read_float(nb_profile) + endline() + + # npts (4th line) + newline() + _ = read_int(nb_profile) + endline() + + # Data + ip_to_r = lambda i: iprofiles[ + next( + filter( + lambda k: k[0] <= i <= k[1], + iprofiles + ) + ) + ] + ip_to_ri = lambda r, i: i - reach_offset[r] + + ts = set() + ind = 0 + end = False + + newline() + while not end: + n = read_int(1)[0] + timestamp = read_float64(1)[0] + + logger.debug(f"read_gra: timestamp = {timestamp} sec") + ts.add(timestamp) + + endline() + + for i in range(n): + newline() + nsl = read_int(1)[0] + endline() + + # Get current profile id + reach = ip_to_r(i) + ri = ip_to_ri(reach, i) + + if nsl > 1: + logger.warning( + "read_gra: " + + "Multiple sediment layers for one profile " + + "is not implemented yet..." + ) + + for j in range(nsl): + newline() + nl = read_int(1)[0] + endline() + + sl = [] + + for k in range(nl): + newline() + data = read_float64(3) + endline() + + h = data[0] + d50 = data[1] + sigma = data[2] + + sl.append((h, d50, sigma)) + + reach.set(ri, timestamp, "sl", sl) + + ind += 1 + end = newline().size <= 0 + + logger.debug(reachs[0].profiles[0]._data) + results.set("timestamps", ts) + logger.info(f"read_gra: ... end with {len(ts)} timestamp read") + + @timer + def results(self, study, repertory, qlog = None): + results = super(Mage8, self).results(study, repertory, qlog) + self.read_gra(study, repertory, results, qlog) + + return results From d7a8e5de20758ac1b69cfba58fd98d22bfa194e0 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 21 Aug 2023 17:21:10 +0200 Subject: [PATCH 155/289] Results, Sediment: Add sediment results plots. --- src/View/Results/PlotSedProfile.py | 130 ++++++++++++++++++++++++++++ src/View/Results/PlotSedReach.py | 134 +++++++++++++++++++++++++++++ src/View/Results/Window.py | 41 +++++++++ src/View/ui/Results.ui | 28 +++++- 4 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/View/Results/PlotSedProfile.py create mode 100644 src/View/Results/PlotSedReach.py diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py new file mode 100644 index 00000000..5b34bb1e --- /dev/null +++ b/src/View/Results/PlotSedProfile.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_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): + super(PlotSedProfile, self).__init__( + canvas=canvas, + data=study, + toolbar=toolbar + ) + + self._results = results + + self._current_timestamp = max(results.get("timestamps")) + self._current_reach_id = reach_id + self._current_profile_id = profile_id + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + reach = self._results.river.reach(self._current_reach_id) + profile = reach.profile(self._current_profile_id) + if profile.geometry.number_points == 0: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "X (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + x = profile.geometry.get_station() + z = profile.geometry.z() + + psl = list( + map( + lambda sl: sl[0], + profile.get_ts_key(self._current_timestamp, "sl") + ) + ) + + sl = [] + for i in range(len(psl)): + cur = [] + for p in range(profile.geometry.number_points): + cur.append(psl[i]) + sl.append(cur) + + # logger.info(sl) + + self.canvas.axes.set_xlim( + left = min(x), right = max(x) + ) + + # Compute sediment layer in function to point z + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl, + [z] + ) + + self.line_kp_sl = [] + for i, zsl in enumerate(reversed(z_sl)): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + x, zsl, + linestyle="solid" if i == len(z_sl) - 1 else "--", + lw=1.8, + color='grey' if i == len(z_sl) - 1 else None + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = False + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + if ind is None: + logger.info("TODO: Update") + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.canvas.draw_idle() + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + self.draw() + + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.draw() diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py new file mode 100644 index 00000000..892120c6 --- /dev/null +++ b/src/View/Results/PlotSedReach.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +import logging + +from functools import reduce + +from tools import timer +from View.Plot.APlot import APlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +_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): + super(PlotSedReach, self).__init__( + canvas=canvas, + data=study, + toolbar=toolbar + ) + + self._results = results + + self._current_timestamp = max(results.get("timestamps")) + self._current_reach_id = reach_id + self._current_profile_id = profile_id + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if self.data is None: + return + + reach = self._results.river.reach(self._current_reach_id) + if reach.geometry.number_profiles == 0: + return + + self.canvas.axes.set_xlabel( + _translate("MainWindow_reach", "Kp (m)"), + color='green', fontsize=12 + ) + self.canvas.axes.set_ylabel( + _translate("MainWindow_reach", "Height (m)"), + color='green', fontsize=12 + ) + + kp = reach.geometry.get_kp() + z_min = reach.geometry.get_z_min() + z_max = reach.geometry.get_z_max() + + psl = list( + map( + # Get SL list for profile p + lambda p: p.get_ts_key(self._current_timestamp, "sl"), + reach.profiles + ) + ) + + max_sl = reduce( + lambda acc, sl: max(acc, len(sl)), + psl, + 0 + ) + + sl = [] + for i in range(max_sl): + cur = [] + for csl in psl: + if i < len(csl): + cur.append(csl[i][0]) + else: + cur.append(0) + sl.append(cur) + + # logger.info(f"sl = {sl}") + + self.canvas.axes.set_xlim( + left = min(kp), right = max(kp) + ) + + # Compute sediment layer in function to profile z_min + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl, + [z_min] + ) + + self.line_kp_sl = [] + for i, z in enumerate(reversed(z_sl)): + self.line_kp_sl.append(None) + self.line_kp_sl[i], = self.canvas.axes.plot( + kp, z, + linestyle="solid" if i == len(z_sl) - 1 else "--", + lw=1.8, + color='grey' if i == len(z_sl) - 1 else None + ) + + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + if self.toolbar is not None: + self.toolbar.update() + + self._init = False + + @timer + def update(self, ind=None): + if self._init == False: + self.draw() + return + + def set_reach(self, reach_id): + self._current_reach_id = reach_id + self._current_profile_id = 0 + self.draw() + + def set_profile(self, profile_id): + self._current_profile_id = profile_id + self.draw() + + def set_timestamp(self, timestamp): + self._current_timestamp = timestamp + self.draw() diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 22d0a2c4..6ca6fc56 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -46,6 +46,8 @@ from View.Results.PlotXY import PlotXY from View.Results.PlotAC import PlotAC from View.Results.PlotKPC import PlotKPC from View.Results.PlotH import PlotH +from View.Results.PlotSedReach import PlotSedReach +from View.Results.PlotSedProfile import PlotSedProfile from View.Results.Table import TableModel from View.Results.translate import * @@ -180,6 +182,37 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): ) self.plot_h.draw() + + self.canvas_5 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_5.setObjectName("canvas_5") + self.plot_layout_5 = self.find(QVBoxLayout, "verticalLayout_sed_reach") + self.plot_layout_5.addWidget(self.canvas_5) + + self.plot_sed_reach = PlotSedReach( + canvas = self.canvas_5, + results = self._results, + study = self._study, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_sed_reach.draw() + + self.canvas_6 = MplCanvas(width=5, height=4, dpi=100) + self.canvas_6.setObjectName("canvas_6") + self.plot_layout_6 = self.find(QVBoxLayout, "verticalLayout_sed_profile") + self.plot_layout_6.addWidget(self.canvas_6) + + self.plot_sed_profile = PlotSedProfile( + canvas = self.canvas_6, + results = self._results, + study = self._study, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_sed_profile.draw() + def setup_connections(self): self.undo_sc.activated.connect(self.undo) self.redo_sc.activated.connect(self.redo) @@ -210,21 +243,29 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_ac.set_reach(reach_id) self.plot_kpc.set_reach(reach_id) self.plot_h.set_reach(reach_id) + self.plot_sed_reach.set_reach(reach_id) + self.plot_sed_profile.set_reach(reach_id) if profile_id is not None: self.plot_xy.set_profile(profile_id) self.plot_ac.set_profile(profile_id) self.plot_kpc.set_profile(profile_id) self.plot_h.set_profile(profile_id) + self.plot_sed_reach.set_profile(profile_id) + self.plot_sed_profile.set_profile(profile_id) if timestamp is not None: self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) self.plot_kpc.set_timestamp(timestamp) self.plot_h.set_timestamp(timestamp) + self.plot_sed_reach.set_timestamp(timestamp) + self.plot_sed_profile.set_timestamp(timestamp) self.plot_xy.draw() self.plot_ac.draw() self.plot_kpc.draw() self.plot_h.draw() + self.plot_sed_reach.draw() + self.plot_sed_profile.draw() def _set_current_reach(self): diff --git a/src/View/ui/Results.ui b/src/View/ui/Results.ui index d975641b..5b797724 100644 --- a/src/View/ui/Results.ui +++ b/src/View/ui/Results.ui @@ -30,7 +30,7 @@ - + @@ -75,6 +75,32 @@ + + + true + + + Sediment + + + + + + true + + + Qt::Vertical + + + + + + + + + + + From bab1ce9e177998a33a5b21581e2f74496764849c Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 22 Aug 2023 11:03:50 +0200 Subject: [PATCH 156/289] Results: Display custom time in hydrograph. --- src/Solver/Mage.py | 2 +- src/View/Results/PlotH.py | 52 +++++++++++++++++++++++-- src/View/ui/Widgets/extendedTimeEdit.ui | 3 ++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 9bc9331e..a9273176 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -201,7 +201,7 @@ class Mage(AbstractSolver): f.write(f"*{header[0]:>9}|{header[1]:>10}\n") for d in bound.data: - f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n") + f.write(f"{d[0]:10}{d[1]:10}\n") return files diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py index dcc9f7f6..47c3f1bf 100644 --- a/src/View/Results/PlotH.py +++ b/src/View/Results/PlotH.py @@ -19,6 +19,7 @@ import logging from functools import reduce +from datetime import datetime from tools import timer, trace from View.Plot.APlot import APlot @@ -34,14 +35,14 @@ logger = logging.getLogger() class PlotH(APlot): def __init__(self, canvas=None, results=None, reach_id=0, profile_id=0, - toolbar=None, display_current=True): + toolbar=None): super(PlotH, self).__init__( canvas=canvas, data=results, toolbar=toolbar ) - self.display_current = display_current + self._mode = "time" self._current_timestamp = max(results.get("timestamps")) self._current_reach_id = reach_id @@ -99,6 +100,10 @@ class PlotH(APlot): ) return + self.canvas.axes.set_ylim( + [min(min(y),0), max(y)] + ) + self._line = [ self.canvas.axes.plot( x, y, lw=1., @@ -107,8 +112,47 @@ class PlotH(APlot): ) ] - self.canvas.axes.autoscale_view(True, True, True) - self.canvas.axes.autoscale() + # Custom time display + nb = len(x) + mod = int(nb / 5) + mod = mod if mod > 0 else nb + + fx = list( + map( + lambda x: x[1], + filter( + lambda x: x[0] % mod == 0, + enumerate(x) + ) + ) + ) + + if self._mode == "time": + t0 = datetime.fromtimestamp(0) + xt = list( + map( + lambda v: ( + str( + datetime.fromtimestamp(v) - t0 + ).split(",")[0]\ + .replace("days", _translate("Results", "days"))\ + .replace("day", _translate("Results", "day")) + ), + fx + ) + ) + else: + xt = list( + map( + lambda v: str(datetime.fromtimestamp(v).date()), + fx + ) + ) + + self.canvas.axes.set_xticks(ticks=fx, labels=xt, rotation=45) + + # self.canvas.axes.autoscale_view(True, True, True) + # self.canvas.axes.autoscale() self.canvas.figure.tight_layout() self.canvas.figure.canvas.draw_idle() if self.toolbar is not None: diff --git a/src/View/ui/Widgets/extendedTimeEdit.ui b/src/View/ui/Widgets/extendedTimeEdit.ui index 493a96a1..eddf3892 100644 --- a/src/View/ui/Widgets/extendedTimeEdit.ui +++ b/src/View/ui/Widgets/extendedTimeEdit.ui @@ -28,6 +28,9 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 999999 + From c1ff99797ceac24ddbf5a70296b3c5b6578c4f24 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 22 Aug 2023 11:39:08 +0200 Subject: [PATCH 157/289] Resutls: Read and display sediment results only if available. --- src/Solver/Mage.py | 4 +-- src/View/Results/Window.py | 63 ++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index a9273176..aa68802f 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -677,8 +677,8 @@ class Mage8(Mage): @timer def read_gra(self, study, repertory, results, qlog = None): - # if not study.river.has_sediment(): - # return + if not study.river.has_sediment(): + return logger.info(f"read_gra: Start ...") diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 6ca6fc56..3882a7ba 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -182,36 +182,37 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): ) self.plot_h.draw() - self.canvas_5 = MplCanvas(width=5, height=4, dpi=100) self.canvas_5.setObjectName("canvas_5") self.plot_layout_5 = self.find(QVBoxLayout, "verticalLayout_sed_reach") self.plot_layout_5.addWidget(self.canvas_5) - self.plot_sed_reach = PlotSedReach( - canvas = self.canvas_5, - results = self._results, - study = self._study, - reach_id = 0, - profile_id = 0, - toolbar = None - ) - self.plot_sed_reach.draw() + if self._study.river.has_sediment(): + self.plot_sed_reach = PlotSedReach( + canvas = self.canvas_5, + results = self._results, + study = self._study, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_sed_reach.draw() self.canvas_6 = MplCanvas(width=5, height=4, dpi=100) self.canvas_6.setObjectName("canvas_6") self.plot_layout_6 = self.find(QVBoxLayout, "verticalLayout_sed_profile") self.plot_layout_6.addWidget(self.canvas_6) - self.plot_sed_profile = PlotSedProfile( - canvas = self.canvas_6, - results = self._results, - study = self._study, - reach_id = 0, - profile_id = 0, - toolbar = None - ) - self.plot_sed_profile.draw() + if self._study.river.has_sediment(): + self.plot_sed_profile = PlotSedProfile( + canvas = self.canvas_6, + results = self._results, + study = self._study, + reach_id = 0, + profile_id = 0, + toolbar = None + ) + self.plot_sed_profile.draw() def setup_connections(self): self.undo_sc.activated.connect(self.undo) @@ -243,29 +244,37 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_ac.set_reach(reach_id) self.plot_kpc.set_reach(reach_id) self.plot_h.set_reach(reach_id) - self.plot_sed_reach.set_reach(reach_id) - self.plot_sed_profile.set_reach(reach_id) + + if self._study.river.has_sediment(): + self.plot_sed_reach.set_reach(reach_id) + self.plot_sed_profile.set_reach(reach_id) if profile_id is not None: self.plot_xy.set_profile(profile_id) self.plot_ac.set_profile(profile_id) self.plot_kpc.set_profile(profile_id) self.plot_h.set_profile(profile_id) - self.plot_sed_reach.set_profile(profile_id) - self.plot_sed_profile.set_profile(profile_id) + + if self._study.river.has_sediment(): + self.plot_sed_reach.set_profile(profile_id) + self.plot_sed_profile.set_profile(profile_id) if timestamp is not None: self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) self.plot_kpc.set_timestamp(timestamp) self.plot_h.set_timestamp(timestamp) - self.plot_sed_reach.set_timestamp(timestamp) - self.plot_sed_profile.set_timestamp(timestamp) + + if self._study.river.has_sediment(): + self.plot_sed_reach.set_timestamp(timestamp) + self.plot_sed_profile.set_timestamp(timestamp) self.plot_xy.draw() self.plot_ac.draw() self.plot_kpc.draw() self.plot_h.draw() - self.plot_sed_reach.draw() - self.plot_sed_profile.draw() + + if self._study.river.has_sediment(): + self.plot_sed_reach.draw() + self.plot_sed_profile.draw() def _set_current_reach(self): From 798e442fc098e8b820b4afa05b27de0aea4b7193 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 22 Aug 2023 11:43:03 +0200 Subject: [PATCH 158/289] tools: Minor change. --- tools/license.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/license.el b/tools/license.el index a35e4308..cdc37cbf 100644 --- a/tools/license.el +++ b/tools/license.el @@ -38,4 +38,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . + +# -*- coding: utf-8 -*- " filename)))) From e8515db8eb1a53f903c4fe9983519da2c511f07a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 22 Aug 2023 14:01:51 +0200 Subject: [PATCH 159/289] Mage: Fix HYD file export (time from second to minute). --- src/Solver/Mage.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index aa68802f..2740ea74 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -201,7 +201,13 @@ class Mage(AbstractSolver): f.write(f"*{header[0]:>9}|{header[1]:>10}\n") for d in bound.data: - f.write(f"{d[0]:10}{d[1]:10}\n") + v0 = d[0] + v1 = d[1] + + if t is "HYD": + v0 /= 60 # Convert first column to minute + + f.write(f"{v0:10}{v1:10}\n") return files From 6b64e5cc99c3f1cf5414fe4d000e2d0998e1139d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 22 Aug 2023 15:33:32 +0200 Subject: [PATCH 160/289] Results, Sediment: Change sediment display algorithme. --- src/View/Results/PlotSedProfile.py | 41 ++++++++++++++++++----- src/View/Results/PlotSedReach.py | 54 +++++++++++++++++++++--------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py index 5b34bb1e..8f77ee3b 100644 --- a/src/View/Results/PlotSedProfile.py +++ b/src/View/Results/PlotSedProfile.py @@ -56,7 +56,7 @@ class PlotSedProfile(APlot): x = profile.geometry.get_station() z = profile.geometry.z() - psl = list( + profiles_sl = list( map( lambda sl: sl[0], profile.get_ts_key(self._current_timestamp, "sl") @@ -64,10 +64,10 @@ class PlotSedProfile(APlot): ) sl = [] - for i in range(len(psl)): + for i in range(len(profiles_sl)): cur = [] for p in range(profile.geometry.number_points): - cur.append(psl[i]) + cur.append(profiles_sl[i]) sl.append(cur) # logger.info(sl) @@ -76,19 +76,44 @@ class PlotSedProfile(APlot): left = min(x), right = max(x) ) - # Compute sediment layer in function to point z + # Dummy layer with height = 0 + f = list(map(lambda p: 0, range(profile.geometry.number_points))) + + # We compute Z sediment layer in reverse order, from last layer to + # fake river bottom + r_sl = list(reversed(sl)) z_sl = reduce( lambda acc, v: acc + [ list( - map(lambda x, y: y - x, v, acc[-1]) + map(lambda x, y: y + x, v, acc[-1]) ) ], - sl, - [z] + r_sl, + [f] + ) + + # We normalize Z coordinate to 0 (the maximum must be 0) + f_z_max = max(z_sl[-1]) + z_sl = list( + map( + lambda p: list(map(lambda z: z - f_z_max, p)), + z_sl + ) + ) + + # We apply the river geometry bottom height at each layers to + # fond the new river geometry + z_sl = list( + map( + lambda sl: list( + map(lambda pz, m: pz + m, sl, z) + ), + z_sl + ) ) self.line_kp_sl = [] - for i, zsl in enumerate(reversed(z_sl)): + for i, zsl in enumerate(z_sl): self.line_kp_sl.append(None) self.line_kp_sl[i], = self.canvas.axes.plot( x, zsl, diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py index 892120c6..d88e26df 100644 --- a/src/View/Results/PlotSedReach.py +++ b/src/View/Results/PlotSedReach.py @@ -56,7 +56,7 @@ class PlotSedReach(APlot): z_min = reach.geometry.get_z_min() z_max = reach.geometry.get_z_max() - psl = list( + profiles_sl = list( map( # Get SL list for profile p lambda p: p.get_ts_key(self._current_timestamp, "sl"), @@ -64,41 +64,65 @@ class PlotSedReach(APlot): ) ) - max_sl = reduce( + max_sl_num = reduce( lambda acc, sl: max(acc, len(sl)), - psl, + profiles_sl, 0 ) sl = [] - for i in range(max_sl): + for i in range(max_sl_num): cur = [] - for csl in psl: - if i < len(csl): - cur.append(csl[i][0]) + for profile_sl in profiles_sl: + if i < len(profile_sl): + cur.append(profile_sl[i][0]) else: cur.append(0) sl.append(cur) - # logger.info(f"sl = {sl}") - self.canvas.axes.set_xlim( - left = min(kp), right = max(kp) + left = min(kp) - 10, right = max(kp) + 10 ) - # Compute sediment layer in function to profile z_min + # Dummy layer with height = 0 + f = list(map(lambda p: 0, reach.profiles)) + + # We compute Z sediment layer in reverse order, from last layer to + # fake river bottom + r_sl = list(reversed(sl)) z_sl = reduce( lambda acc, v: acc + [ list( - map(lambda x, y: y - x, v, acc[-1]) + map(lambda x, y: y + x, v, acc[-1]) ) ], - sl, - [z_min] + r_sl, + [f] ) + # We normalize Z coordinate to 0 (the maximum must be 0) + f_z_max = max(z_sl[-1]) + z_sl = list( + map( + lambda p: list(map(lambda z: z - f_z_max, p)), + z_sl + ) + ) + + # We apply the river geometry bottom height at each layers to + # fond the new river geometry + z_sl = list( + map( + lambda sl: list( + map(lambda z, m: z + m, sl, z_min) + ), + z_sl + ) + ) + + # Draw self.line_kp_sl = [] - for i, z in enumerate(reversed(z_sl)): + for i, z in enumerate(z_sl): self.line_kp_sl.append(None) self.line_kp_sl[i], = self.canvas.axes.plot( kp, z, From 0d105f7488846d549e8aec5ebdcb323a017872a4 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 23 Aug 2023 11:23:06 +0200 Subject: [PATCH 161/289] Sediment: Change sediment results display (again)... --- src/View/Results/PlotSedProfile.py | 126 ++++++++++------- src/View/Results/PlotSedReach.py | 219 ++++++++++++++++++++--------- 2 files changed, 231 insertions(+), 114 deletions(-) diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py index 8f77ee3b..62f4e290 100644 --- a/src/View/Results/PlotSedProfile.py +++ b/src/View/Results/PlotSedProfile.py @@ -31,6 +31,80 @@ class PlotSedProfile(APlot): self._current_reach_id = reach_id self._current_profile_id = profile_id + def get_zsl(self, profile): + x = profile.geometry.get_station() + z = profile.geometry.z() + + profiles_sl = list( + map( + lambda sl: sl[0], + profile.get_ts_key(self._current_timestamp, "sl") + ) + ) + profiles_sl_0 = list( + map( + lambda sl: sl[0], + profile.get_ts_key(0.0, "sl") + ) + ) + + f = list(map(lambda p: 0, range(profile.geometry.number_points))) + + sl = [] + sl_0 = [] + for profile_sl, profile_sl_0 in zip(profiles_sl, profiles_sl_0): + cur = [] + cur_0 = [] + + for p in range(profile.geometry.number_points): + cur.append(profile_sl) + cur_0.append(profile_sl_0) + + sl.append(cur) + sl_0.append(cur_0) + + # Compute sediment layer from initial data in function to + # profile z_min + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl_0, + [z] + ) + + # Diff between initial data and data att current timestamp + d_sl = list( + map( + lambda ln0, lni: list( + map( + lambda z0, zi: z0 - zi, + ln0, lni + ) + ), + sl_0, sl + ) + ) + + # Apply diff for t0 for each layer Z + z_sl = list( + map( + lambda z, d: list( + map( + lambda zn, dn: zn - dn, + z, d + ) + ), + z_sl, + d_sl + [f] # HACK: Add dummy data for last layer + ) + ) + + return list(reversed(z_sl)) + + @timer def draw(self): self.canvas.axes.cla() @@ -56,61 +130,11 @@ class PlotSedProfile(APlot): x = profile.geometry.get_station() z = profile.geometry.z() - profiles_sl = list( - map( - lambda sl: sl[0], - profile.get_ts_key(self._current_timestamp, "sl") - ) - ) - - sl = [] - for i in range(len(profiles_sl)): - cur = [] - for p in range(profile.geometry.number_points): - cur.append(profiles_sl[i]) - sl.append(cur) - - # logger.info(sl) - self.canvas.axes.set_xlim( left = min(x), right = max(x) ) - # Dummy layer with height = 0 - f = list(map(lambda p: 0, range(profile.geometry.number_points))) - - # We compute Z sediment layer in reverse order, from last layer to - # fake river bottom - r_sl = list(reversed(sl)) - z_sl = reduce( - lambda acc, v: acc + [ - list( - map(lambda x, y: y + x, v, acc[-1]) - ) - ], - r_sl, - [f] - ) - - # We normalize Z coordinate to 0 (the maximum must be 0) - f_z_max = max(z_sl[-1]) - z_sl = list( - map( - lambda p: list(map(lambda z: z - f_z_max, p)), - z_sl - ) - ) - - # We apply the river geometry bottom height at each layers to - # fond the new river geometry - z_sl = list( - map( - lambda sl: list( - map(lambda pz, m: pz + m, sl, z) - ), - z_sl - ) - ) + z_sl = self.get_zsl(profile) self.line_kp_sl = [] for i, zsl in enumerate(z_sl): diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py index d88e26df..657b70e8 100644 --- a/src/View/Results/PlotSedReach.py +++ b/src/View/Results/PlotSedReach.py @@ -31,6 +31,161 @@ class PlotSedReach(APlot): self._current_reach_id = reach_id self._current_profile_id = profile_id + # DEPRECATED version of sediment layser display + # def _get_zsl(self, reach): + # kp = reach.geometry.get_kp() + # z_min = reach.geometry.get_z_min() + # z_max = reach.geometry.get_z_max() + + # profiles_sl = list( + # map( + # # Get SL list for profile p + # lambda p: p.get_ts_key(self._current_timestamp, "sl"), + # reach.profiles + # ) + # ) + + # max_sl_num = reduce( + # lambda acc, sl: max(acc, len(sl)), + # profiles_sl, + # 0 + # ) + + # sl = [] + # for i in range(max_sl_num): + # cur = [] + # for profile_sl in profiles_sl: + # if i < len(profile_sl): + # cur.append(profile_sl[i][0]) + # else: + # cur.append(0) + # sl.append(cur) + + # self.canvas.axes.set_xlim( + # left = min(kp) - 10, right = max(kp) + 10 + # ) + + # # Dummy layer with height = 0 + # f = list(map(lambda p: 0, reach.profiles)) + + # # We compute Z sediment layer in reverse order, from last layer to + # # fake river bottom + # r_sl = list(reversed(sl)) + # z_sl = reduce( + # lambda acc, v: acc + [ + # list( + # map(lambda x, y: y + x, v, acc[-1]) + # ) + # ], + # r_sl, + # [f] + # ) + + # # We normalize Z coordinate to 0 (the maximum must be 0) + # f_z_max = max(z_sl[-1]) + # z_sl = list( + # map( + # lambda p: list(map(lambda z: z - f_z_max, p)), + # z_sl + # ) + # ) + + # # We apply the river geometry bottom height at each layers to + # # fond the new river geometry + # z_sl = list( + # map( + # lambda sl: list( + # map(lambda z, m: z + m, sl, z_min) + # ), + # z_sl + # ) + # ) + + # return z_sl + + def get_zsl(self, reach): + kp = reach.geometry.get_kp() + z_min = reach.geometry.get_z_min() + z_max = reach.geometry.get_z_max() + + profiles_sl_0 = list( + map( + # Get SL list for profile p at time 0 (initial data) + lambda p: p.get_ts_key(0.0, "sl"), + reach.profiles + ) + ) + profiles_sl = list( + map( + # Get SL list for profile p at current time + lambda p: p.get_ts_key(self._current_timestamp, "sl"), + reach.profiles + ) + ) + + max_sl_num = reduce( + lambda acc, sl: max(acc, len(sl)), + profiles_sl, + 0 + ) + f = list(map(lambda p: 0, reach.profiles)) + + sl = [] + sl_0 = [] + for i in range(max_sl_num): + cur = [] + cur_0 = [] + for profile_sl, profile_sl_0 in zip(profiles_sl, profiles_sl_0): + if i < len(profile_sl_0): + cur.append(profile_sl[i][0]) + cur_0.append(profile_sl_0[i][0]) + else: + cur.append(0) + cur_0.append(0) + sl.append(cur) + sl_0.append(cur_0) + + # Compute sediment layer from initial data in function to + # profile z_min + z_sl = reduce( + lambda acc, v: acc + [ + list( + map(lambda x, y: y - x, v, acc[-1]) + ) + ], + sl_0, + [z_min] + ) + + # Diff between initial data and data att current timestamp + d_sl = list( + map( + lambda ln0, lni: list( + map( + lambda z0, zi: z0 - zi, + ln0, lni + ) + ), + sl_0, sl + ) + ) + + # Apply diff for t0 for each layer Z + z_sl = list( + map( + lambda z, d: list( + map( + lambda zn, dn: zn - dn, + z, d + ) + ), + z_sl, + d_sl + [f] # HACK: Add dummy data for last layer + ) + ) + + return list(reversed(z_sl)) + @timer def draw(self): self.canvas.axes.cla() @@ -56,69 +211,7 @@ class PlotSedReach(APlot): z_min = reach.geometry.get_z_min() z_max = reach.geometry.get_z_max() - profiles_sl = list( - map( - # Get SL list for profile p - lambda p: p.get_ts_key(self._current_timestamp, "sl"), - reach.profiles - ) - ) - - max_sl_num = reduce( - lambda acc, sl: max(acc, len(sl)), - profiles_sl, - 0 - ) - - sl = [] - for i in range(max_sl_num): - cur = [] - for profile_sl in profiles_sl: - if i < len(profile_sl): - cur.append(profile_sl[i][0]) - else: - cur.append(0) - sl.append(cur) - - self.canvas.axes.set_xlim( - left = min(kp) - 10, right = max(kp) + 10 - ) - - # Dummy layer with height = 0 - f = list(map(lambda p: 0, reach.profiles)) - - # We compute Z sediment layer in reverse order, from last layer to - # fake river bottom - r_sl = list(reversed(sl)) - z_sl = reduce( - lambda acc, v: acc + [ - list( - map(lambda x, y: y + x, v, acc[-1]) - ) - ], - r_sl, - [f] - ) - - # We normalize Z coordinate to 0 (the maximum must be 0) - f_z_max = max(z_sl[-1]) - z_sl = list( - map( - lambda p: list(map(lambda z: z - f_z_max, p)), - z_sl - ) - ) - - # We apply the river geometry bottom height at each layers to - # fond the new river geometry - z_sl = list( - map( - lambda sl: list( - map(lambda z, m: z + m, sl, z_min) - ), - z_sl - ) - ) + z_sl = self.get_zsl(reach) # Draw self.line_kp_sl = [] From 98c5a6b610f28b06fc15ef916f17cad7bafcc9d4 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 23 Aug 2023 15:17:04 +0200 Subject: [PATCH 162/289] Plot: More flexible matplotlib toolbar. --- src/View/Geometry/mainwindow_ui_reach.py | 9 +- src/View/Plot/navigation_toolbar_2qt.py | 125 +++++++++++++---------- 2 files changed, 79 insertions(+), 55 deletions(-) diff --git a/src/View/Geometry/mainwindow_ui_reach.py b/src/View/Geometry/mainwindow_ui_reach.py index 747b6ca4..897b6987 100644 --- a/src/View/Geometry/mainwindow_ui_reach.py +++ b/src/View/Geometry/mainwindow_ui_reach.py @@ -191,7 +191,8 @@ class Ui_MainWindow(object): self.canvas_1.setObjectName("canvas_1") self.toolbar_1 = navigation_toolbar_2qt.PamHyrNavigationToolbar2QT( - self.canvas_1, self.centralwidget + self.canvas_1, self.centralwidget, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] ) self.verticalLayout_canvas_1.addWidget(self.toolbar_1) @@ -207,7 +208,8 @@ class Ui_MainWindow(object): self.canvas_2.setObjectName("canvas_2") self.toolbar_2 = navigation_toolbar_2qt.PamHyrNavigationToolbar2QT( - self.canvas_2, self.centralwidget + self.canvas_2, self.centralwidget, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] ) self.verticalLayout_canvas_2.addWidget(self.toolbar_2) @@ -222,7 +224,8 @@ class Ui_MainWindow(object): self.canvas_3.setObjectName("canvas_3") self.toolbar_3 = navigation_toolbar_2qt.PamHyrNavigationToolbar2QT( - self.canvas_3, self.centralwidget + self.canvas_3, self.centralwidget, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] ) self.verticalLayout_canvas_3.addWidget(self.toolbar_3) diff --git a/src/View/Plot/navigation_toolbar_2qt.py b/src/View/Plot/navigation_toolbar_2qt.py index 1cca7cf0..a2f6ce31 100644 --- a/src/View/Plot/navigation_toolbar_2qt.py +++ b/src/View/Plot/navigation_toolbar_2qt.py @@ -38,70 +38,91 @@ class PamHyrNavigationToolbar2QT(NavigationToolbar2QT): """ isometric_signal = pyqtSignal(str) - def __init__(self, canvas, parent): + def __init__(self, canvas, parent, items = ["home", "move", "zoom", "save"]): """ Args: canvas: canvas de matplotlib parent: parent du canvas """ self.my_canvas = canvas + + self._items = items self.toolitems = [ (None, None, None, None), - - ('Home', _translate("Toolbar", 'Vue originale'), 'home', 'home'), - (None, None, None, None), - - ('Back', _translate("Toolbar", 'Retour à la vue précédente'), 'back', 'back'), - ('Forward', _translate("Toolbar", 'Passer à la vue suivante'), 'forward', 'forward'), - (None, None, None, None), - - ('Pan', _translate( - "Toolbar", - 'Panoramique des axes avec la souris gauche, zoom avec la droite' - ), 'move', 'pan'), - (None, None, None, None), - - ('Zoom', _translate("Toolbar", 'Zoom'), 'zoom_to_rect', 'zoom'), - (None, None, None, None), - - ('Isometric_view', _translate( - "Toolbar", 'Vue isométrique (Shift+W)' - ), '', 'isometric_view'), - (None, None, None, None), - - ('GlobalView', _translate( - "Toolbar", 'Vue globale automatique (Shift+X)' - ), '', 'non_isometric_view'), - - (None, None, None, None), - - ('Save', _translate( - "Toolbar", 'Enregistrer la figure' - ), 'filesave', 'save_figure'), - (None, None, None, None), ] + icons = [] + + if "home" in items: + 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((None, None, None, None)) + + if "move" in items: + self.toolitems.append( + ('Pan', _translate( + "Toolbar", + 'Axes panoramic' + ), 'move', 'pan')) + 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((None, None, None, None)) + + icon_zoom = QtGui.QIcon() + icon_zoom.addPixmap(QtGui.QPixmap( + f"{os.path.dirname(os.path.dirname(file_path))}/resources/icons/zoom.png" + )) + + icons.append(("zoom",icon_zoom)) + + if "iso" in items: + self.toolitems.append( + ('Isometric_view', _translate( + "Toolbar", 'Isometric view (Shift+W)' + ), '', 'isometric_view') + ) + self.toolitems.append((None, None, None, None)) + + self.toolitems.append( + ('GlobalView', _translate( + "Toolbar", 'Auto scale view (Shift+X)' + ), '', 'non_isometric_view') + ) + self.toolitems.append((None, None, None, None)), + + icon_btn_isometric_view = QtGui.QIcon() + icon_btn_isometric_view.addPixmap( + QtGui.QPixmap(f"{os.path.dirname(os.path.dirname(file_path))}/resources" + f"/icons/zoom_fit_11.png")) + + icon_btn_global_view = QtGui.QIcon() + icon_btn_global_view.addPixmap( + QtGui.QPixmap(f"{os.path.dirname(os.path.dirname(file_path))}/resources/icons" + f"/zoom_fit.png")) + + icons.append(("isometric_view", icon_btn_isometric_view)) + icons.append(("non_isometric_view", icon_btn_global_view)) + + if "save" in items: + self.toolitems.append( + ('Save', _translate( + "Toolbar", 'Save the figure' + ), 'filesave', 'save_figure') + ) + self.toolitems.append((None, None, None, None)) + NavigationToolbar2QT.__init__(self, canvas, parent) btn_size = QSize(30, 30) - - icon_zoom = QtGui.QIcon() - icon_zoom.addPixmap(QtGui.QPixmap( - f"{os.path.dirname(os.path.dirname(file_path))}/resources/icons/zoom.png" - )) - icon_btn_isometric_view = QtGui.QIcon() - icon_btn_isometric_view.addPixmap( - QtGui.QPixmap(f"{os.path.dirname(os.path.dirname(file_path))}/resources" - f"/icons/zoom_fit_11.png")) - - icon_btn_global_view = QtGui.QIcon() - icon_btn_global_view.addPixmap( - QtGui.QPixmap(f"{os.path.dirname(os.path.dirname(file_path))}/resources/icons" - f"/zoom_fit.png")) - actions = self.findChildren(QAction) - self._actions["zoom"].setIcon(icon_zoom) - self._actions["isometric_view"].setIcon(icon_btn_isometric_view) - self._actions["non_isometric_view"].setIcon(icon_btn_global_view) + + for a, i in icons: + self._actions[a].setIcon(i) self.addSeparator() @@ -124,7 +145,7 @@ class PamHyrNavigationToolbar2QT(NavigationToolbar2QT): fname, filter = qt_compat._getSaveFileName( self.canvas.parent(), - _translate("MainWindow_reach", "Choisissez un nom de fichier à sauvegarder"), + _translate("MainWindow_reach", "Select destination file"), start, filters, selectedFilter) From 2842bb5339eb4104d7c8c1412d902ab18b55ef15 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 23 Aug 2023 15:35:41 +0200 Subject: [PATCH 163/289] Results: Add toolbar to each plot. --- src/View/Results/Window.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 3882a7ba..bd491f37 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -41,6 +41,7 @@ from PyQt5.QtWidgets import ( ) from View.Plot.MplCanvas import MplCanvas +from View.Plot.navigation_toolbar_2qt import PamHyrNavigationToolbar2QT from View.Results.PlotXY import PlotXY from View.Results.PlotAC import PlotAC @@ -127,7 +128,11 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): def setup_graph(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") + self.toolbar = PamHyrNavigationToolbar2QT( + self.canvas, self + ) self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) self.plot_layout.addWidget(self.canvas) self.plot_xy = PlotXY( @@ -135,14 +140,18 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): results = self._results, reach_id = 0, profile_id = 0, - toolbar = None, + toolbar = self.toolbar, display_current = False ) self.plot_xy.draw() self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) self.canvas_2.setObjectName("canvas_2") + self.toolbar_2 = PamHyrNavigationToolbar2QT( + self.canvas_2, self + ) self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout_2.addWidget(self.toolbar_2) self.plot_layout_2.addWidget(self.canvas_2) self.plot_kpc = PlotKPC( @@ -150,13 +159,17 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): results = self._results, reach_id = 0, profile_id = 0, - toolbar = None + toolbar = self.toolbar_2 ) self.plot_kpc.draw() self.canvas_3 = MplCanvas(width=5, height=4, dpi=100) self.canvas_3.setObjectName("canvas_3") + self.toolbar_3 = PamHyrNavigationToolbar2QT( + self.canvas_3, self + ) self.plot_layout_3 = self.find(QVBoxLayout, "verticalLayout_3") + self.plot_layout_3.addWidget(self.toolbar_3) self.plot_layout_3.addWidget(self.canvas_3) self.plot_ac = PlotAC( @@ -164,13 +177,17 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): results = self._results, reach_id = 0, profile_id = 0, - toolbar = None + toolbar = self.toolbar_3 ) self.plot_ac.draw() self.canvas_4 = MplCanvas(width=5, height=4, dpi=100) self.canvas_4.setObjectName("canvas_4") + self.toolbar_4 = PamHyrNavigationToolbar2QT( + self.canvas_4, self + ) 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( @@ -178,13 +195,17 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): results = self._results, reach_id = 0, profile_id = 0, - toolbar = None + toolbar = self.toolbar_4 ) self.plot_h.draw() self.canvas_5 = MplCanvas(width=5, height=4, dpi=100) self.canvas_5.setObjectName("canvas_5") + self.toolbar_5 = PamHyrNavigationToolbar2QT( + self.canvas_5, self + ) self.plot_layout_5 = self.find(QVBoxLayout, "verticalLayout_sed_reach") + self.plot_layout_5.addWidget(self.toolbar_5) self.plot_layout_5.addWidget(self.canvas_5) if self._study.river.has_sediment(): @@ -194,13 +215,17 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): study = self._study, reach_id = 0, profile_id = 0, - toolbar = None + toolbar = self.toolbar_5 ) self.plot_sed_reach.draw() self.canvas_6 = MplCanvas(width=5, height=4, dpi=100) self.canvas_6.setObjectName("canvas_6") + self.toolbar_6 = PamHyrNavigationToolbar2QT( + self.canvas_6, self + ) 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(): @@ -210,7 +235,7 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): study = self._study, reach_id = 0, profile_id = 0, - toolbar = None + toolbar = self.toolbar_6 ) self.plot_sed_profile.draw() From 2de2c8e077e369232cdb351d2f528acb64945072 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 23 Aug 2023 16:17:45 +0200 Subject: [PATCH 164/289] Pamhyr: Add plot toolbar in most of window. --- src/View/BoundaryCondition/Edit/Window.py | 6 +++++ src/View/InitialConditions/Window.py | 14 +++++++++-- src/View/LateralContribution/Edit/Window.py | 6 +++++ src/View/Results/PlotH.py | 2 +- src/View/SedimentLayers/Edit/Window.py | 23 ++++++++++++++++++- .../SedimentLayers/Reach/Profile/Window.py | 9 ++++++-- src/View/SedimentLayers/Reach/Window.py | 9 ++++++-- 7 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index efcf0993..652aa22d 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -37,6 +37,7 @@ from PyQt5.QtWidgets import ( ) from View.Plot.MplCanvas import MplCanvas +from View.Plot.navigation_toolbar_2qt import PamHyrNavigationToolbar2QT from View.BoundaryCondition.translate import long_types from View.BoundaryCondition.Edit.Table import TableModel, ExTimeDelegate @@ -110,12 +111,17 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): def setup_plot(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") + self.toolbar = PamHyrNavigationToolbar2QT( + self.canvas, self + ) + self.verticalLayout.addWidget(self.toolbar) self.verticalLayout.addWidget(self.canvas) self.plot = Plot( canvas = self.canvas, data = self._data, mode = self._study.time_system, + toolbar = self.toolbar, ) self.plot.draw() diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 95f6004a..2d4e8077 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -49,6 +49,8 @@ from View.InitialConditions.UndoCommand import ( from View.InitialConditions.Table import TableModel, ComboBoxDelegate from View.Plot.MplCanvas import MplCanvas +from View.Plot.navigation_toolbar_2qt import PamHyrNavigationToolbar2QT + from View.InitialConditions.PlotDKP import PlotDKP from View.InitialConditions.PlotDischarge import PlotDischarge from View.InitialConditions.translate import * @@ -120,25 +122,33 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): def setup_graph(self): self.canvas_1 = MplCanvas(width=5, height=4, dpi=100) self.canvas_1.setObjectName("canvas_1") + self.toolbar_1 = PamHyrNavigationToolbar2QT( + self.canvas_1, self + ) self.plot_layout_1 = self.find(QVBoxLayout, "verticalLayout_1") + self.plot_layout_1.addWidget(self.toolbar_1) self.plot_layout_1.addWidget(self.canvas_1) self.plot_1 = PlotDKP( canvas = self.canvas_1, data = self._ics, - toolbar = None, + toolbar = self.toolbar_1, ) self.plot_1.draw() self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) self.canvas_2.setObjectName("canvas_2") + self.toolbar_2 = PamHyrNavigationToolbar2QT( + self.canvas_2, self + ) self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout_2.addWidget(self.toolbar_2) self.plot_layout_2.addWidget(self.canvas_2) self.plot_2 = PlotDischarge( canvas = self.canvas_2, data = self._ics, - toolbar = None, + toolbar = self.toolbar_2, ) self.plot_2.draw() diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 3a801591..413667d0 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -37,6 +37,7 @@ from PyQt5.QtWidgets import ( ) from View.Plot.MplCanvas import MplCanvas +from View.Plot.navigation_toolbar_2qt import PamHyrNavigationToolbar2QT from View.LateralContribution.translate import long_types from View.LateralContribution.Edit.Table import TableModel, ExTimeDelegate @@ -110,12 +111,17 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): def setup_plot(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") + self.toolbar = PamHyrNavigationToolbar2QT( + self.canvas, self + ) + self.verticalLayout.addWidget(self.toolbar) self.verticalLayout.addWidget(self.canvas) self.plot = Plot( canvas = self.canvas, data = self._data, mode = self._study.time_system, + toolbar = self.toolbar, ) self.plot.draw() diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py index 47c3f1bf..557c7cec 100644 --- a/src/View/Results/PlotH.py +++ b/src/View/Results/PlotH.py @@ -101,7 +101,7 @@ class PlotH(APlot): return self.canvas.axes.set_ylim( - [min(min(y),0), max(y)] + [min(min(y),0), max(y) + 10] ) self._line = [ diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index 965895d2..ac141ce7 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -1,3 +1,19 @@ +# 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 @@ -25,6 +41,7 @@ from PyQt5.QtWidgets import ( ) from View.Plot.MplCanvas import MplCanvas +from View.Plot.navigation_toolbar_2qt import PamHyrNavigationToolbar2QT from View.SedimentLayers.Edit.UndoCommand import * from View.SedimentLayers.Edit.Table import * @@ -89,7 +106,11 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def setup_graph(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") + self.toolbar = PamHyrNavigationToolbar2QT( + self.canvas, self + ) self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) self.plot_layout.addWidget(self.canvas) self._set_plot() @@ -97,7 +118,7 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot = Plot( canvas = self.canvas, data = self._sl, - toolbar = None, + toolbar = self.toolbar, display_current = False ) self.plot.draw() diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index d1dd5e69..aafd719b 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -29,8 +29,9 @@ from View.SedimentLayers.Reach.Profile.Table import * from View.SedimentLayers.Reach.Profile.Plot import Plot from View.Plot.MplCanvas import MplCanvas -from View.SedimentLayers.Reach.Profile.translate import * +from View.Plot.navigation_toolbar_2qt import PamHyrNavigationToolbar2QT +from View.SedimentLayers.Reach.Profile.translate import * from View.SedimentLayers.Window import SedimentLayersWindow _translate = QCoreApplication.translate @@ -109,7 +110,11 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def setup_graph(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") + self.toolbar = PamHyrNavigationToolbar2QT( + self.canvas, self + ) self.plot_layout = self.find(QVBoxLayout, "verticalLayout") + self.plot_layout.addWidget(self.toolbar) self.plot_layout.addWidget(self.canvas) self._update_plot() @@ -118,7 +123,7 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot = Plot( canvas = self.canvas, data = self._profile, - toolbar = None, + toolbar = self.toolbar, display_current = False ) self.plot.draw() diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index ee477edc..f70049ae 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -30,8 +30,9 @@ from View.SedimentLayers.Reach.Plot import Plot from View.SedimentLayers.Reach.SLDialog import SLDialog from View.Plot.MplCanvas import MplCanvas -from View.SedimentLayers.Reach.translate import * +from View.Plot.navigation_toolbar_2qt import PamHyrNavigationToolbar2QT +from View.SedimentLayers.Reach.translate import * from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Profile.Window import ProfileSedimentLayersWindow @@ -97,7 +98,11 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): def setup_graph(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") + self.toolbar = PamHyrNavigationToolbar2QT( + self.canvas, self + ) self.plot_layout = self.find(QVBoxLayout, "verticalLayout_2") + self.plot_layout.addWidget(self.toolbar) self.plot_layout.addWidget(self.canvas) self._update_plot() @@ -106,7 +111,7 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.plot = Plot( canvas = self.canvas, data = self._reach, - toolbar = None, + toolbar = self.toolbar, display_current = False ) self.plot.draw() From 09bdb7dd92f97d3fcf28f012a322383d7d5f6a4e Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 23 Aug 2023 16:44:14 +0200 Subject: [PATCH 165/289] Mage: Fix QSO export. --- src/Solver/Mage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 2740ea74..3101e4eb 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -204,7 +204,7 @@ class Mage(AbstractSolver): v0 = d[0] v1 = d[1] - if t is "HYD": + if t in ["HYD", "QSO"]: v0 /= 60 # Convert first column to minute f.write(f"{v0:10}{v1:10}\n") From 5e5cc8d050dc788870db664f86aa93e3b1fe5fed Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 25 Aug 2023 10:20:18 +0200 Subject: [PATCH 166/289] Results: Add status bar timestamp information. --- src/View/Results/Window.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index bd491f37..14e8abee 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -17,6 +17,7 @@ # -*- coding: utf-8 -*- import logging +from datetime import datetime from tools import trace, timer @@ -37,7 +38,7 @@ from PyQt5.QtWidgets import ( QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, QComboBox, QVBoxLayout, QHeaderView, QTabWidget, - QSlider, + QSlider, QLabel, ) from View.Plot.MplCanvas import MplCanvas @@ -78,6 +79,7 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.setup_table() self.setup_graph() self.setup_slider() + self.setup_statusbar() self.setup_connections() self.ui.setWindowTitle(self._title) @@ -239,6 +241,27 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): ) self.plot_sed_profile.draw() + def _compute_status_label(self): + ts = self._timestamps[self._slider_time.value()] + + t0 = datetime.fromtimestamp(0) + fts = str( + datetime.fromtimestamp(ts) - t0 + ) + fts.replace("days", _translate("Results", "days"))\ + .replace("day", _translate("Results", "day")) + + return (f"Timestamp : {fts} ({ts} sec)") + + def setup_statusbar(self): + txt = self._compute_status_label() + self._status_label = QLabel(txt) + self.statusbar.addPermanentWidget(self._status_label) + + def update_statusbar(self): + txt = self._compute_status_label() + self._status_label.setText(txt) + def setup_connections(self): self.undo_sc.activated.connect(self.undo) self.redo_sc.activated.connect(self.redo) @@ -286,7 +309,7 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) self.plot_kpc.set_timestamp(timestamp) - self.plot_h.set_timestamp(timestamp) + # self.plot_h.set_timestamp(timestamp) if self._study.river.has_sediment(): self.plot_sed_reach.set_timestamp(timestamp) @@ -301,6 +324,7 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_sed_reach.draw() self.plot_sed_profile.draw() + self.update_statusbar() def _set_current_reach(self): table = self.find(QTableView, f"tableView_reach") From ebbe4c4be71460deb23067be8b1c2b6be1e9c7fe Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 25 Aug 2023 10:58:10 +0200 Subject: [PATCH 167/289] Resutls: Synchronized table and slider. --- src/View/Results/Window.py | 55 +++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 14e8abee..e788a65f 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -31,6 +31,7 @@ from PyQt5.QtGui import ( from PyQt5.QtCore import ( Qt, QVariant, QAbstractTableModel, QCoreApplication, QModelIndex, pyqtSlot, + QItemSelectionModel, ) from PyQt5.QtWidgets import ( @@ -242,6 +243,7 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self.plot_sed_profile.draw() def _compute_status_label(self): + # Timestamp ts = self._timestamps[self._slider_time.value()] t0 = datetime.fromtimestamp(0) @@ -251,7 +253,27 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): fts.replace("days", _translate("Results", "days"))\ .replace("day", _translate("Results", "day")) - return (f"Timestamp : {fts} ({ts} sec)") + # Reach + table = self.find(QTableView, f"tableView_reach") + indexes = table.selectedIndexes() + if len(indexes) == 0: + reach = self._study.river.edges()[0] + else: + reach = self._study.river.edges()[indexes[0].row()] + + # Profile + table = self.find(QTableView, f"tableView_profile") + indexes = table.selectedIndexes() + if len(indexes) == 0: + profile = reach.reach.profile(0) + else: + profile = reach.reach.profile(indexes[0].row()) + + pname = profile.name if profile.name != "" else profile.kp + + return (f"Reach: {reach.name} | " + + f"Profile: {pname} | " + + f"Timestamp : {fts} ({ts} sec)") def setup_statusbar(self): txt = self._compute_status_label() @@ -285,6 +307,31 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): 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): + table = self.find(QTableView, f"tableView_reach") + selectionModel = table.selectionModel() + index = table.model().index(ind, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) + + def update_table_selection_profile(self, ind): + table = self.find(QTableView, f"tableView_profile") + selectionModel = table.selectionModel() + index = table.model().index(ind, 0) + + selectionModel.select( + index, + QItemSelectionModel.Rows | + QItemSelectionModel.ClearAndSelect | + QItemSelectionModel.Select + ) + table.scrollTo(index) def update(self, reach_id = None, profile_id = None, timestamp = None): if reach_id is not None: @@ -296,6 +343,10 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): if self._study.river.has_sediment(): self.plot_sed_reach.set_reach(reach_id) self.plot_sed_profile.set_reach(reach_id) + + self.update_table_selection_reach(reach_id) + self.update_table_selection_profile(0) + if profile_id is not None: self.plot_xy.set_profile(profile_id) self.plot_ac.set_profile(profile_id) @@ -305,6 +356,8 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): if self._study.river.has_sediment(): self.plot_sed_reach.set_profile(profile_id) self.plot_sed_profile.set_profile(profile_id) + + self.update_table_selection_profile(profile_id) if timestamp is not None: self.plot_xy.set_timestamp(timestamp) self.plot_ac.set_timestamp(timestamp) From 3b5b7c09dd5424620ee189bbb9fdbf8028b83bb8 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 25 Aug 2023 11:15:33 +0200 Subject: [PATCH 168/289] Results, River: Minor change. --- src/Model/River.py | 4 ---- src/View/RunSolver/Window.py | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Model/River.py b/src/Model/River.py index 91d765ba..b1969543 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -311,10 +311,6 @@ class River(Graph, SQLSubModel): self._save_submodel(execute, objs, data) return True - @property - def reach(self): - return self._reach - @property def frictions(self): return self._frictions diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index c307157a..56290b6c 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -166,6 +166,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_stop").setEnabled(True) self.find(QAction, "action_log_file").setEnabled(False) + self.find(QAction, "action_results").setEnabled(False) def setup_alarm(self): self._alarm = QTimer() @@ -201,6 +202,7 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_pause").setEnabled(False) self.find(QAction, "action_stop").setEnabled(False) self.find(QAction, "action_results").setEnabled(True) + if self._solver.log_file() != "": self.find(QAction, "action_log_file").setEnabled(True) From f9c28034a592ed3755ad290cafee17212312af8b Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 28 Aug 2023 10:30:32 +0200 Subject: [PATCH 169/289] Mage: Add PAR export ligne filter and disable 'command_line_arguments'. --- src/Solver/Mage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 3101e4eb..c53d5f75 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -478,6 +478,9 @@ class Mage8(Mage): .replace("mage_", "") value = p.value + if name in ["command_line_arguments"]: + continue + f.write(f"{name} {value}\n") return files From d064fc042a8af7053e40a0bbb22ca84e26aa63d6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 28 Aug 2023 11:12:03 +0200 Subject: [PATCH 170/289] Mage: Change file name export from '0.XXX' to '.XXX'. --- src/Solver/ASolver.py | 3 + src/Solver/Mage.py | 128 +++++++++++++++++++++++------------------- 2 files changed, 74 insertions(+), 57 deletions(-) diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py index 186abbbb..a87dad97 100644 --- a/src/Solver/ASolver.py +++ b/src/Solver/ASolver.py @@ -67,6 +67,9 @@ class AbstractSolver(object): self._process = None self._output = None + # Last study running + self._study = None + def __str__(self): return f"{self._name} : {self._type} : {self._description}" diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index c53d5f75..61c8b26f 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -105,18 +105,23 @@ class Mage(AbstractSolver): return l def input_param(self): - return "0.REP" + name = self._study.name + return f"{name}.REP" def log_file(self): - return "0.TRA" + name = self._study.name + return f"{name}.TRA" @timer - def _export_ST(self, study, repertory, qlog): + def _export_ST(self, study, repertory, qlog, name="0"): files = [] if qlog is not None: qlog.put("Export ST file") + os.makedirs(os.path.join(repertory, "net"), exist_ok=True) + gra_file = f"{name}.GRA" + # Write header edges = study.river.edges() edges = list( @@ -131,8 +136,8 @@ class Mage(AbstractSolver): if edge._name == "": name = f"Reach_{edge.id}" - with mage_file_open(os.path.join(repertory, f"{name}.ST"), "w+") as f: - files.append(f"{name}.ST") + with mage_file_open(os.path.join(repertory, "net", f"{name}.ST"), "w+") as f: + files.append(str(os.path.join("net", f"{name}.ST"))) cnt_num = 1 for profile in edge.reach.profiles: @@ -149,7 +154,7 @@ class Mage(AbstractSolver): sediment = "" if profile.sl is not None: if not any(filter(lambda f: ".GRA" in f, files)): - files.append("0.GRA") + files.append(gra_file) nl = len(profile.sl) sediment = f" {nl:>3}" @@ -181,7 +186,7 @@ class Mage(AbstractSolver): return files @timer - def _export_BC(self, t, bounds, repertory, qlog): + def _export_BC(self, t, bounds, repertory, qlog, name="0"): files = [] if len(bounds) == 0: @@ -190,8 +195,8 @@ class Mage(AbstractSolver): if qlog is not None: qlog.put(f"Export {t} file") - with mage_file_open(os.path.join(repertory, f"0.{t}"), "w+") as f: - files.append(f"0.{t}") + with mage_file_open(os.path.join(repertory, f"{name}.{t}"), "w+") as f: + files.append(f"{name}.{t}") for bound in bounds: name = f"{bound.node.id:3}".replace(" ", "x") @@ -204,7 +209,7 @@ class Mage(AbstractSolver): v0 = d[0] v1 = d[1] - if t in ["HYD", "QSO"]: + if t in ["HYD", "QSO", "LIM"]: v0 /= 60 # Convert first column to minute f.write(f"{v0:10}{v1:10}\n") @@ -212,7 +217,7 @@ class Mage(AbstractSolver): return files @timer - def _export_bound_cond(self, study, repertory, qlog): + def _export_bound_cond(self, study, repertory, qlog, name="0"): files = [] lst = study.river.boundary_condition @@ -232,22 +237,22 @@ class Mage(AbstractSolver): elif bound.bctype == "SL": QSO.append(bound) - files = files + self._export_BC("AVA", AVA, repertory, qlog) - files = files + self._export_BC("HYD", HYD, repertory, qlog) - files = files + self._export_BC("LIM", LIM, repertory, qlog) - files = files + self._export_QSO(QSO, repertory, qlog) + files = files + self._export_BC("AVA", AVA, repertory, qlog, name=name) + files = files + self._export_BC("HYD", HYD, repertory, qlog, name=name) + files = files + self._export_BC("LIM", LIM, repertory, qlog, name=name) + files = files + self._export_QSO(QSO, repertory, qlog, name=name) return files # @timer - # def _export_LC(self, lateral, repertory, qlog): + # def _export_LC(self, lateral, repertory, qlog, name="0"): # files = [] # if qlog is not None: # qlog.put(f"Export LAT file") - # with mage_file_open(os.path.join(repertory, f"0.LAT"), "w+") as f: - # files.append(f"0.LAT") + # with mage_file_open(os.path.join(repertory, f"{name}.LAT"), "w+") as f: + # files.append(f"{name}.LAT") # name = f"{lateral.node.id:3}".replace(" ", "x") # f.write(f"* {lateral.node.name} ({name}) {lateral.bctype}\n") @@ -256,12 +261,12 @@ class Mage(AbstractSolver): # f.write(f"*{header[0]:>9}|{header[1]:>10}\n") # for d in lateral.data: - # f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n") + # f.write(f"{d[0]:1{name}.3f}{d[1]:10.3f}\n") # return files # @timer - # def _export_lateral_contrib(self, study, repertory, qlog): + # def _export_lateral_contrib(self, study, repertory, qlog, name="0"): # files = [] # lst = study.river.lateral_contribution @@ -272,15 +277,15 @@ class Mage(AbstractSolver): # return files @timer - def _export_RUG(self, study, repertory, qlog): + def _export_RUG(self, study, repertory, qlog, name="0"): files = [] if qlog is not None: qlog.put("Export RUG file") # Write header - with mage_file_open(os.path.join(repertory, "0.RUG"), "w+") as f: - files.append("0.RUG") + with mage_file_open(os.path.join(repertory, f"{name}.RUG"), "w+") as f: + files.append(f"{name}.RUG") edges = study.river.edges() edges = list( @@ -313,14 +318,14 @@ class Mage(AbstractSolver): return files @timer - def _export_INI(self, study, repertory, qlog): + def _export_INI(self, study, repertory, qlog, name="0"): files = [] if qlog is not None: qlog.put("Export INI file") # Write header - with mage_file_open(os.path.join(repertory, "0.INI"), "w+") as f: + with mage_file_open(os.path.join(repertory, f"{name}.INI"), "w+") as f: has_ini = False id = 1 reachs = study.river.edges() @@ -358,16 +363,16 @@ class Mage(AbstractSolver): id += 1 if has_ini: - files.append("0.INI") + files.append(f"{name}.INI") return files @timer - def _export_REP(self, study, repertory, files, qlog): + def _export_REP(self, study, repertory, files, qlog, name="0"): if qlog is not None: qlog.put("Export REP file") # Write header - with mage_file_open(os.path.join(repertory, f"0.REP"), "w+") as f: + with mage_file_open(os.path.join(repertory, f"{name}.REP"), "w+") as f: f.write("confirmation=non\n") for file in files: @@ -377,8 +382,8 @@ class Mage(AbstractSolver): f.write(f"{EXT} {file}\n") f.write("* OUTPUT\n") - f.write(f"TRA 0.TRA\n") - f.write(f"BIN 0.BIN\n") + f.write(f"TRA {name}.TRA\n") + f.write(f"BIN {name}.BIN\n") for file in files: EXT = file.split('.')[1] @@ -389,7 +394,10 @@ class Mage(AbstractSolver): @timer def export(self, study, repertory, qlog = None): - self._export_ST(study, repertory, qlog) + self._study = study + name = study.name.replace(" ", "_") + + self._export_ST(study, repertory, qlog, name = name) return True @@ -397,11 +405,11 @@ class Mage(AbstractSolver): # RESULTS # ########### - def read_bin(self, study, repertory, results, qlog = None): + def read_bin(self, study, repertory, results, qlog = None, name="0"): return @timer - def results(self, study, repertory, qlog = None): + def results(self, study, repertory, qlog = None, name="0"): results = Results(study = study) self.read_bin(study, repertory, results, qlog) @@ -462,14 +470,14 @@ class Mage8(Mage): return l @timer - def _export_PAR(self, study, repertory, qlog = None): + def _export_PAR(self, study, repertory, qlog = None, name = "0"): files = [] if qlog is not None: qlog.put("Export PAR file") - with mage_file_open(os.path.join(repertory, "0.PAR"), "w+") as f: - files.append("0.PAR") + with mage_file_open(os.path.join(repertory, f"{name}.PAR"), "w+") as f: + files.append(f"{name}.PAR") params = study.river.get_params(self.type).parameters for p in params: @@ -486,14 +494,14 @@ class Mage8(Mage): return files @timer - def _export_NET(self, study, repertory, qlog = None): + def _export_NET(self, study, repertory, qlog = None, name="0"): files = [] if qlog is not None: qlog.put("Export NET file") - with mage_file_open(os.path.join(repertory, "0.NET"), "w+") as f: - files.append("0.NET") + with mage_file_open(os.path.join(repertory, f"{name}.NET"), "w+") as f: + files.append(f"{name}.NET") edges = study.river.edges() edges = list( @@ -512,14 +520,14 @@ class Mage8(Mage): n1 = f"{e.node1.id:3}".replace(" ", "x") n2 = f"{e.node2.id:3}".replace(" ", "x") - file = name + ".ST" + file = os.path.join("net", name + ".ST") f.write(f"{id} {n1} {n2} {file}\n") return files @timer - def _export_QSO(self, bounds, repertory, qlog): + def _export_QSO(self, bounds, repertory, qlog, name="0"): files = [] if len(bounds) == 0: @@ -528,8 +536,8 @@ class Mage8(Mage): if qlog is not None: qlog.put(f"Export QSO file") - with mage_file_open(os.path.join(repertory, f"0.QSO"), "w+") as f: - files.append(f"0.QSO") + with mage_file_open(os.path.join(repertory, f"{name}.QSO"), "w+") as f: + files.append(f"{name}.QSO") for bound in bounds: name = f"{bound.node.id:3}".replace(" ", "x") @@ -545,16 +553,19 @@ class Mage8(Mage): @timer - def export(self, study, repertory, qlog = None): + def export(self, study, repertory, qlog = None, name="0"): + self._study = study + name = study.name.replace(" ", "_") + files = [] - files = self._export_ST(study, repertory, qlog) - files = files + self._export_PAR(study, repertory, qlog) - files = files + self._export_NET(study, repertory, qlog) - files = files + self._export_bound_cond(study, repertory, qlog) - files = files + self._export_RUG(study, repertory, qlog) - files = files + self._export_INI(study, repertory, qlog) - self._export_REP(study, repertory, files, qlog) + 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_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) return True @@ -563,10 +574,10 @@ class Mage8(Mage): ########### @timer - def read_bin(self, study, repertory, results, qlog = None): + def read_bin(self, study, repertory, results, qlog = None, name="0"): logger.info(f"read_bin: Start ...") - with mage_file_open(os.path.join(repertory, f"0.BIN"), "r") as f: + with mage_file_open(os.path.join(repertory, f"{name}.BIN"), "r") as f: newline = lambda: np.fromfile(f, dtype=np.int32, count=1) endline = lambda: np.fromfile(f, dtype=np.int32, count=1) @@ -685,13 +696,13 @@ class Mage8(Mage): logger.info(f"read_bin: ... end with {len(ts)} timestamp read") @timer - def read_gra(self, study, repertory, results, qlog = None): + def read_gra(self, study, repertory, results, qlog = None, name="0"): if not study.river.has_sediment(): return logger.info(f"read_gra: Start ...") - with mage_file_open(os.path.join(repertory, f"0.GRA"), "r") as f: + with mage_file_open(os.path.join(repertory, f"{name}.GRA"), "r") as f: newline = lambda: np.fromfile(f, dtype=np.int32, count=1) endline = lambda: np.fromfile(f, dtype=np.int32, count=1) @@ -838,7 +849,10 @@ class Mage8(Mage): @timer def results(self, study, repertory, qlog = None): - results = super(Mage8, self).results(study, repertory, qlog) - self.read_gra(study, repertory, results, qlog) + 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) return results From cbaeeaed83bdd29aa9f1b67150ccc253d1cddced Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 28 Aug 2023 11:56:51 +0200 Subject: [PATCH 171/289] Mage, RunSolver: Minor change. --- src/Solver/Mage.py | 2 +- src/View/RunSolver/Window.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 61c8b26f..33d63ae7 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -412,7 +412,7 @@ class Mage(AbstractSolver): def results(self, study, repertory, qlog = None, name="0"): results = Results(study = study) - self.read_bin(study, repertory, results, qlog) + self.read_bin(study, repertory, results, qlog, name=name) return results diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 56290b6c..226d4da5 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -257,6 +257,8 @@ class SolverLogWindow(ASubMainWindow, ListedSubWindow): def results(self): 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.open_solver_results(self._solver, self._results) def log_file(self): From 79d8bb7dee68d6b6083ef97c706a738e4c279b7e Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 28 Aug 2023 15:34:37 +0200 Subject: [PATCH 172/289] Mage: Logging BIN and GRA name in reading methode. --- src/Solver/Mage.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 33d63ae7..d75fd6cb 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -575,9 +575,10 @@ class Mage8(Mage): @timer def read_bin(self, study, repertory, results, qlog = None, name="0"): - logger.info(f"read_bin: Start ...") + fname = os.path.join(repertory, f"{name}.BIN") + logger.info(f"read_bin: Start reading '{fname}' ...") - with mage_file_open(os.path.join(repertory, f"{name}.BIN"), "r") as f: + 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) @@ -700,9 +701,10 @@ class Mage8(Mage): if not study.river.has_sediment(): return - logger.info(f"read_gra: Start ...") + fname = os.path.join(repertory, f"{name}.GRA") + logger.info(f"read_gra: Start reading '{fname}' ...") - with mage_file_open(os.path.join(repertory, f"{name}.GRA"), "r") as f: + 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) From e189ab69dc972dc3c746501f351d577dda0f93d5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 29 Aug 2023 13:37:39 +0200 Subject: [PATCH 173/289] Mage: Add sediment parameter into solver parameter and update DB. --- .../SolverParameters/SolverParametersList.py | 53 +++++++++++++++++++ src/Model/Study.py | 2 +- src/Solver/Mage.py | 14 +++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py index 18c45529..4aae2b37 100644 --- a/src/Model/SolverParameters/SolverParametersList.py +++ b/src/Model/SolverParameters/SolverParametersList.py @@ -100,6 +100,59 @@ class SolverParametersList(SQLSubModel): 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") + + for solver in solvers: + if solver is not "mage8": + continue + + data = execute("SELECT ind, name, value " + + "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)}'" + + ")" + ) + + ind = 0 + lst = [] + for v in data: + insert(v[0], v[1], ind) + ind += 1 + + new = [ + ("mage_sediment_masse_volumique", "2650.0"), + ("mage_sediment_angle_repos", "40.0"), + ("mage_sediment_porosity", "0.40"), + ("mage_distance_han", "0.0"), + ("mage_distance_chargement_d50", "100.0"), + ("mage_distance_chargement_sigma", "100.0"), + ("mage_methode_modification_geometrie", "1"), + ("mage_shields_critique", "1"), + ("mage_shields_correction", "1"), + ("mage_capacite_solide", "1"), + ("mage_pas_de_temps_charriage", "1"), + ("mage_facteur_multiplicateur", "1.0"), + ] + + for v in new: + insert(v[0], v[1], ind) + ind += 1 + return cls._update_submodel(execute, version) @classmethod diff --git a/src/Model/Study.py b/src/Model/Study.py index 5b44d0a7..774a03d1 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -40,7 +40,7 @@ class Study(SQLModel): def __init__(self, filename = None, init_new = True): # Metadata - self._version = "0.0.3" + self._version = "0.0.4" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index d75fd6cb..8fdf25a0 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -455,6 +455,20 @@ class Mage8(Mage): i = names.index("mage_precision_reduction_factor_Q") lst.insert(i+1, ("mage_precision_reduction_factor_r", "1")) + # Mage parameter for sediment module (added in DB 0.0.4) + lst.append(("mage_sediment_masse_volumique", "2650.0")) + lst.append(("mage_sediment_angle_repos", "40.0")) + lst.append(("mage_sediment_porosity", "0.40")) + lst.append(("mage_distance_Han", "0.0")) + lst.append(("mage_distance_chargement_d50", "100.0")) + lst.append(("mage_distance_chargement_sigma", "100.0")) + lst.append(("mage_methode_modification_geometrie", "1")) + lst.append(("mage_shields_critique", "1")) + lst.append(("mage_shields_correction", "1")) + lst.append(("mage_capacite_solide", "1")) + lst.append(("mage_pas_de_temps_charriage", "1")) + lst.append(("mage_facteur_multiplicateur", "1.0")) + return lst ########## From fb9c2a4308067336325963ba8bea81805f7bb140 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 30 Aug 2023 09:27:25 +0200 Subject: [PATCH 174/289] Plot: Fix toolbar image and translate. --- src/Solver/Mage.py | 3 ++- src/View/Plot/navigation_toolbar_2qt.py | 25 ++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 8fdf25a0..697f8809 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -556,7 +556,8 @@ class Mage8(Mage): for bound in bounds: name = f"{bound.node.id:3}".replace(" ", "x") f.write(f"* {bound.node.name} ({name}) {bound.bctype}\n") - f.write(f"${name}\n") + f.write(f"${name} 0.002 1 default\n") + # f.write(f"${name}\n") header = bound.header f.write(f"*{header[0]:>9}|{header[1]:>10}\n") diff --git a/src/View/Plot/navigation_toolbar_2qt.py b/src/View/Plot/navigation_toolbar_2qt.py index a2f6ce31..ec57078d 100644 --- a/src/View/Plot/navigation_toolbar_2qt.py +++ b/src/View/Plot/navigation_toolbar_2qt.py @@ -76,9 +76,11 @@ class PamHyrNavigationToolbar2QT(NavigationToolbar2QT): icon_zoom = QtGui.QIcon() icon_zoom.addPixmap(QtGui.QPixmap( - f"{os.path.dirname(os.path.dirname(file_path))}/resources/icons/zoom.png" + 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: @@ -98,13 +100,11 @@ class PamHyrNavigationToolbar2QT(NavigationToolbar2QT): icon_btn_isometric_view = QtGui.QIcon() icon_btn_isometric_view.addPixmap( - QtGui.QPixmap(f"{os.path.dirname(os.path.dirname(file_path))}/resources" - f"/icons/zoom_fit_11.png")) + QtGui.QPixmap(os.path.abspath(f"{file_path}/../ui/ressources/zoom_fit_11.png"))) icon_btn_global_view = QtGui.QIcon() icon_btn_global_view.addPixmap( - QtGui.QPixmap(f"{os.path.dirname(os.path.dirname(file_path))}/resources/icons" - f"/zoom_fit.png")) + QtGui.QPixmap(os.path.abspath(f"{file_path}/../ui/ressources/zoom_fit.png"))) icons.append(("isometric_view", icon_btn_isometric_view)) icons.append(("non_isometric_view", icon_btn_global_view)) @@ -175,12 +175,11 @@ class PamHyrNavigationToolbar2QT(NavigationToolbar2QT): self.my_canvas.figure.canvas.draw_idle() def toolitems_translation(self): - self._actions['home'].setToolTip(_translate("Toolbar", "Vue originale")) - self._actions['back'].setToolTip(_translate("Toolbar", "Retour à la vue précédente")) - self._actions['forward'].setToolTip(_translate("Toolbar", "Passer à la vue suivante")) - self._actions['pan'].setToolTip(_translate("Toolbar", "Panoramique des axes avec la " - "souris gauche, zoom avec la droite")) + 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['zoom'].setToolTip(_translate("Toolbar", "Zoom")) - self._actions['save_figure'].setToolTip(_translate("Toolbar", "Enregistrer la figure")) - self.action_isometric_view.setToolTip(_translate("Toolbar", "Vue isométrique (Shift+W)")) - self.action_auto_global_view.setToolTip(_translate("Toolbar", "Vue globale automatique (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)")) From 0c06b4cb2ac026f493a4c68abe4a284b1ab8ec48 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 30 Aug 2023 10:42:45 +0200 Subject: [PATCH 175/289] BC: Add D50 and sigma edition for solid BC. --- .../BoundaryConditionTypes.py | 3 + src/Solver/Mage.py | 14 ++- .../BoundaryCondition/Edit/UndoCommand.py | 30 +++++++ src/View/BoundaryCondition/Edit/Window.py | 87 ++++++++++++++++++- src/View/ui/EditBoundaryConditions.ui | 26 +++--- src/View/ui/Widgets/d50sigma.ui | 70 +++++++++++++++ 6 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 src/View/ui/Widgets/d50sigma.ui diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py index 963e88e7..af3c91fd 100644 --- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -89,6 +89,9 @@ class Solid(BoundaryCondition): def __init__(self, id:int = -1, name:str = "", status = None): super(Solid, self).__init__(id=id, name=name, status=status) + self.d50 = 0.002 + self.sigma = 1 + self._type = "SL" self._header = ["time", "solid"] self._types = [TimeOverDischarge.time_convert, float] diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 697f8809..6ed03e66 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -554,13 +554,23 @@ class Mage8(Mage): files.append(f"{name}.QSO") for bound in bounds: + # File header name = f"{bound.node.id:3}".replace(" ", "x") f.write(f"* {bound.node.name} ({name}) {bound.bctype}\n") - f.write(f"${name} 0.002 1 default\n") - # f.write(f"${name}\n") + + d50 = bound.d50 + sigma = bound.sigma + + if len(bound.data) == 0: + f.write(f"${name} {d50} {sigma} default\n") + else: + f.write(f"${name} {d50} {sigma}\n") + + # Table header header = bound.header f.write(f"*{header[0]:>9}|{header[1]:>10}\n") + # Data for d in bound.data: f.write(f"{d[0]:10.3f}{d[1]:10.3f}\n") diff --git a/src/View/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py index 4bb2605f..b30331a1 100644 --- a/src/View/BoundaryCondition/Edit/UndoCommand.py +++ b/src/View/BoundaryCondition/Edit/UndoCommand.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +import logging + from copy import deepcopy from tools import trace, timer @@ -25,6 +27,8 @@ from PyQt5.QtWidgets import ( from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition +logger = logging.getLogger() + class SetDataCommand(QUndoCommand): def __init__(self, data, index, column, new_value): QUndoCommand.__init__(self) @@ -42,6 +46,32 @@ 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) + + self._data = data + self._column = column + if self._column == "d50": + self._old = self._data.d50 + elif self._column == "sigma": + self._old = self._data.sigma + + self._new = float(new_value) + + def undo(self): + if self._column == "d50": + self._data.d50 = self._old + elif self._column == "sigma": + self._data.sigma = self._old + + def redo(self): + if self._column == "d50": + self._data.d50 = self._new + elif self._column == "sigma": + self._data.sigma = self._new + + class AddCommand(QUndoCommand): def __init__(self, data, index): QUndoCommand.__init__(self) diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 652aa22d..7f35759f 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -18,33 +18,77 @@ from tools import timer, trace -from View.ASubWindow import ASubMainWindow +from View.ASubWindow import ASubMainWindow, AWidget from View.ListedSubWindow import ListedSubWindow from PyQt5.QtGui import ( QKeySequence, ) +from PyQt5 import QtCore from PyQt5.QtCore import ( Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, ) from PyQt5.QtWidgets import ( QDialogButtonBox, QPushButton, QLineEdit, QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, - QHeaderView, + QHeaderView, QDoubleSpinBox, QVBoxLayout, ) from View.Plot.MplCanvas import MplCanvas from View.Plot.navigation_toolbar_2qt import PamHyrNavigationToolbar2QT from View.BoundaryCondition.translate import long_types +from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand from View.BoundaryCondition.Edit.Table import TableModel, ExTimeDelegate from View.BoundaryCondition.Edit.Plot import Plot _translate = QCoreApplication.translate +class WD50Sigma(AWidget): + 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") + + self.spinBox_d50.valueChanged.connect(self.valueChangedD50) + self.spinBox_sigma.valueChanged.connect(self.valueChangedSigma) + + def set_d50(self, d50): + self.spinBox_d50.valueChanged.disconnect(self.valueChangedD50) + self.spinBox_d50.setValue(float(d50)) + self.spinBox_d50.valueChanged.connect(self.valueChangedD50) + + def get_d50(self): + return float(self.spinBox_d50.value()) + + def set_sigma(self, sigma): + self.spinBox_sigma.valueChanged.disconnect(self.valueChangedSigma) + self.spinBox_sigma.setValue(float(sigma)) + self.spinBox_sigma.valueChanged.connect(self.valueChangedSigma) + + def get_sigma(self): + return float(self.spinBox_sigma.value()) + + @QtCore.pyqtSlot(float) + def valueChangedD50(self, value): + self.d50Changed.emit(value) + + @QtCore.pyqtSlot(float) + 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): @@ -63,6 +107,7 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.setup_sc() self.setup_table() self.setup_plot() + self.setup_data() self.setup_connections() def compute_title(self): @@ -84,6 +129,17 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.copy_sc = QShortcut(QKeySequence.Copy, self) self.paste_sc = QShortcut(QKeySequence.Paste, self) + def setup_data(self): + self._is_solid = self._data.bctype == "SL" + + if self._is_solid: + layout = self.find(QVBoxLayout, "verticalLayout_table") + self._d50sigma = WD50Sigma(parent = self) + layout.addWidget(self._d50sigma) + + self._d50sigma.set_d50(self._data.d50) + self._d50sigma.set_sigma(self._data.sigma) + def setup_table(self): table = self.find(QTableView, "tableView") self._table = TableModel( @@ -138,6 +194,31 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self._table.dataChanged.connect(self.update) + if self._is_solid: + self._d50sigma.d50Changed.connect(self.d50_changed) + self._d50sigma.sigmaChanged.connect(self.sigma_changed) + + def d50_changed(self, value): + self._undo_stack.push( + SetMetaDataCommand( + self._data, + "d50", value + ) + ) + + def sigma_changed(self, value): + self._undo_stack.push( + SetMetaDataCommand( + self._data, + "sigma", value + ) + ) + + def widget_update(self): + if self._is_solid: + self._d50sigma.set_d50(self._data.d50) + self._d50sigma.set_sigma(self._data.sigma) + def update(self): self.plot.update() @@ -221,7 +302,9 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): def undo(self): self._table.undo() self.plot.update() + self.widget_update() def redo(self): self._table.redo() self.plot.update() + self.widget_update() diff --git a/src/View/ui/EditBoundaryConditions.ui b/src/View/ui/EditBoundaryConditions.ui index 1fabe5fc..7f615964 100644 --- a/src/View/ui/EditBoundaryConditions.ui +++ b/src/View/ui/EditBoundaryConditions.ui @@ -26,22 +26,22 @@ - - - 0 - 0 - - Qt::Horizontal - - - - 0 - 200 - - + + + + + + + 0 + 200 + + + + + diff --git a/src/View/ui/Widgets/d50sigma.ui b/src/View/ui/Widgets/d50sigma.ui new file mode 100644 index 00000000..d1af908b --- /dev/null +++ b/src/View/ui/Widgets/d50sigma.ui @@ -0,0 +1,70 @@ + + + Form + + + + 0 + 0 + 256 + 46 + + + + Form + + + + + + + + + Qt::Horizontal + + + + + + + D50 + + + + + + + + + + 4 + + + + + + + + + + + Sigma + + + + + + + 4 + + + + + + + + + + + + From 10d6a17a59233c184dcbdc9bd5aac985fd1dd250 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 30 Aug 2023 10:43:26 +0200 Subject: [PATCH 176/289] Mage: Change Y/N into PAR file to O/N (as french). --- src/Solver/Mage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 6ed03e66..1cad3f68 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -503,6 +503,9 @@ class Mage8(Mage): if name in ["command_line_arguments"]: continue + if name is "mage_compute_reach_volume_balance": + value = "O" if value == "y" else "N" + f.write(f"{name} {value}\n") return files From 0dd727f9d4aa4b220b472889ba649924b1520c9d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 30 Aug 2023 10:59:21 +0200 Subject: [PATCH 177/289] packages: Fix linux widget directory. --- packages/linux.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/linux.sh b/packages/linux.sh index 44770b6e..215047e8 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -44,8 +44,9 @@ 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/ +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/lang From 0ec87a7f24e8a684bc1a8f51e414dc35c8bfa9f7 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 30 Aug 2023 11:00:09 +0200 Subject: [PATCH 178/289] trans: Update french translate file. --- src/lang/fr.ts | 608 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 421 insertions(+), 187 deletions(-) diff --git a/src/lang/fr.ts b/src/lang/fr.ts index 9bc3f345..8034809f 100644 --- a/src/lang/fr.ts +++ b/src/lang/fr.ts @@ -41,52 +41,52 @@ Z (m) - + days jours - + day jour - + Not defined Non définie - + Ponctual contribution Contributions ponctuelles - + Time over Z - + Time over Discharge - + Z over Discharge - + Name Nom - + Type Type - + Node Nœud @@ -100,6 +100,16 @@ Discharge (m³/s) + + + Solid (kg/s) + + + + + Solid + Solide + Checker @@ -468,12 +478,12 @@ - + days jours - + HH:mm:ss @@ -502,6 +512,16 @@ ... + + + D50 + + + + + Sigma + + Frictions @@ -617,12 +637,12 @@ - + days jours - + day jour @@ -695,12 +715,12 @@ MainWindow - + MainWindow Fenêtre principale - + toolBar Bar d'outils @@ -765,12 +785,12 @@ &Aide - + toolBar_2 - + New study Nouvelle étude @@ -780,257 +800,257 @@ Ctrl+N - + Open a study Ouvrir une étude - + Ctrl+O Ctrl+O - + Import data from MAGE Importer des données d'une étude MAGE - + Import data from RubarBE Importer des données d'une étude RubarBE - + Close Fermer - + Close current study Fermer l'étude en cours - + Save mesh Sauvegarder le maillage - + Save Sauvegarder - + Ctrl+S Ctrl+S - + Save as ... Sauvegarder sous... - + Ctrl+Shift+S Ctrl+Shift+S - + Archive Archive - + Pamhyr configuration Configuration de PAMHYR - + Quit Quitter - + Ctrl+F4 Ctrl+F4 - + Edit river network Éditer le réseau - + Edit geometry Éditer la géométrie - + Import geometry Importer une géométrie - + Export geometry Exporter la géométrie - + Run extrenal meshing tool Lancer le mailler externe - + choose meshing tool by reach - + View meshed geometry Voir la géométrie mailler - + Export mesh Exporter le maillage - + Delete mesh of current reach Supprimer le maillage - + Delete all mesh - + Abscisse - Cote - + XYZ - + Numerical parameter for solvers Paramètre numerique des solvers - + Boundary conditions and one-time contributions Condition aux limites et apports ponctuels - + Initial conditions Conditions initiales - + Export initial conditions Exporter les conditions initiales - + Import final state as initial condition Importer un état final comme conditions initiales - + Edit friction Éditer les frottements - + Edit lateral contributions Éditer les apports latéraux - + Edit spills - + Edit cross building Éditer les ouvrages - + Run solver Lancer un solver - + F5 F5 - + Stop solver Stopper le solver - + Display listings - + Simulation directory management - + Open Ouvrir - + Hydrograph Hydrogramme - + Limnigram Limnigramme - + Map current reach Cartographier le bief sélectionné - + Help PAMHYR Aide de PAMHYR - + Help MAGE Aide de MAGE - + About A propos - + ouvrir - + Save current study Sauvegarder l'étude - + Ctrl+F Ctrl+F @@ -1040,132 +1060,132 @@ Quitter l'application - + Ctrl+Q Ctrl+Q - + stop solver - + Interrompt la simulation en cours - + Ctrl+C Ctrl+C - + Run external meshing tool Lancer le mailler externe - + Run meshing tool on current reach geometry Lancer le mailler externe sur le bief selectionné - + Display simulation listing - + Display current simulation listing - + River network Réseau - + Geometry Géométrie - + Edit reach geometry Éditer la géométrie - + Mesh Maillage - + Boundary conditions Conditions aux limites - + Edit boundary conditions and one-time contributions Éditer les conditions aux limites et les apports ponctuels - + Lateral contribution Contributions latérales - + Edit lateral contribution Éditer les contributions latérales - + Spills - + Edit lateral spills - + Friction Frottements - + Stricklers - + Edit the study stricklers Éditer les Stricklers de l'étude - + Building Ouvrages - + Edit building (valve, ...), singularity and pump Éditer les ouvrages - + Edit study Éditer l'étude - + English - + French @@ -1370,47 +1390,47 @@ Trier les points des conditions aux limites - + Quit the application (Ctrl+Q) Quitter l'application (Ctrl+Q) - + Run a solver Lancer un solver - + Display meshed reach - + Edit friction frictions Éditer les frottements - + Define initial conditions Définire les conditions initiales - + Édition des Tronçons - + Ouvrir une étude - + Enrégistrer étude en cours (Ctrl+S) - + Fermer étude en cours (Ctrl+F) @@ -1419,6 +1439,106 @@ results + + + Add sediment layer + + + + + Delete sediment layer + + + + + Edit sediment layer + + + + + &Sediment + + + + + Sediment layers + + + + + Edit reach sediment layers + + + + + Add a new sediment layer + + + + + Delete selected sediment layer(s) + + + + + Move up + + + + + Move down + + + + + Sediment + + + + + Edit sediment layers list + + + + + Apply sediment layers on all reach + + + + + Edit profile + + + + + Edit profile sediment layer + + + + + Add sediment layers + + + + + Add specific sediment layers on selected point(s) + + + + + Delete sediment layers + + + + + Delete specific sediment layers of selected point(s) + + + + + Edit sediment layers + + MainWindowProfile @@ -1603,7 +1723,7 @@ sur le plan défini par les deux points nommés extrêmes - + Kp (m) Pk (m) @@ -1648,7 +1768,7 @@ sur le plan défini par les deux points nommés extrêmes - + Cote (m) @@ -1658,68 +1778,68 @@ sur le plan défini par les deux points nommés extrêmes - + Jeu de sections du Bief - + Ordre des sections : Amont --> Aval - + Pk = - + Nouveau profil - + Trier les profils par ordre croissant des Pk - + Name Nom - + Type Type - + Alt+Z - + Alt+E - + Alt+R - + Vue globale automatique (Alt+S) - + Vue globale automatique (Alt+D) - + Vue globale automatique (Alt+F) @@ -1743,11 +1863,6 @@ Ordre des sections : Amont --> Aval Discharge (m^3/s) Débit (m³/s) - - - Choisissez un nom de fichier à sauvegarder - - Édition des profils sélectionnés @@ -1775,67 +1890,67 @@ Seuls les 5 premiers seront édités. - + Importer une géométrie - + Supprimer le profil sélectionné - + Éditer le profil sélectionné - + Copier le profil sélectionné - + Coller le profil en fin de liste (penser à modifier le Pk avant de trier) - + Dupliquer la section sélectionnée - + Trier les profils par ordre décroissant des Pk - + Changer l'ordre des profils (en décalant le profil sélectionné vers le haut) - + Changer l'ordre des profils (en décalant le profil sélectionné vers le bas) - + Terminer l'édition - + Vue isométrique (Alt+Z) - + Vue isométrique (Alt+E) - + Vue isométrique (Alt+R) @@ -1844,6 +1959,16 @@ Seuls les 5 premiers seront édités. Elevation (m) Altitude (m) + + + Height (m) + Hauteur (m) + + + + Select destination file + + Network @@ -1883,15 +2008,15 @@ Seuls les 5 premiers seront édités. Inverser l'orientation du bief - + Results - + X (m) - + Y (m) @@ -1910,6 +2035,110 @@ Seuls les 5 premiers seront édités. KP (m) + + + days + jours + + + + day + jour + + + + Time (s) + + + + + Discharge (m³/s) + + + + + SedimentLayers + + + (no name) + + + + + Name + Nom + + + + Type + Type + + + + (no name - @kp) + + + + + Not defined + Non définie + + + + X (m) + + + + + Y (m) + + + + + Sediment layers + + + + + KP (m) + + + + + Comment + Commentaire + + + + SedimentLazers + + + Z (m) + + + + + Sedimentlayers + + + Height + + + + + D50 + + + + + Sigma + + + + + Critical constraint + + SolverParameters @@ -1959,120 +2188,125 @@ Seuls les 5 premiers seront édités. Pas de temps (en second) - + Minimum timestep (second) Pas de temps minimal (en second) - + Time step of writing on .TRA Pas de temps d'écriture dans le fichier .TRA - + Time step of writing on .BIN Pas de temps d'écriture dans le fichier .BIN - + Implicitation parameter - + Continuity discretization type (S/L) - + QSJ discretization (A/B) - + Stop criterion iterations (G/A/R) - + Iteration type - + Smoothing coefficient - + Maximun accepted number of CFL - + Minimum water height (meter) - + Maximun number of iterations (< 100) - + Timestep reduction factor - + Reduction precision factor of Z - + Reduction precision factor of Q - + Reduction precision factor of residue - + Number of iteration at maximum precision - + Number of iteration before switch - + Maximum accepted Froude number - + Diffluence node height balance - + Compute reach volume balance (Y/N) - + Maximum reach volume balance - + Minimum reach volume to check + + + Command line arguments + + Solvers @@ -2090,43 +2324,43 @@ Seuls les 5 premiers seront édités. Toolbar - - Vue originale - - - - - Panoramique des axes avec la souris gauche, zoom avec la droite - - - - + Zoom - - Vue globale automatique (Shift+X) + + Default view - - Enregistrer la figure + + Back to previous view - - - Retour à la vue précédente + + + Return to next view - - - Passer à la vue suivante + + + Axes panoramic - - - Vue isométrique (Shift+W) + + + Isometric view (Shift+W) + + + + + Auto scale view (Shift+X) + + + + + Save the figure From 0a968e218473fb0653d6f09bb75ad52a967cf2f6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 30 Aug 2023 13:57:28 +0200 Subject: [PATCH 179/289] ci: Update mage version. --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 19d0962c..150f5c0a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,7 +31,7 @@ build-mage-linux: rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG script: - - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/Test5/downloads/packages/mage_linux.tgz + - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/Test6/downloads/packages/mage_linux.tgz - mkdir -p mage - cd mage - tar xvf ../mage.tgz @@ -69,7 +69,7 @@ build-mage-windows: rules: - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_TAG script: - - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/Test5/downloads/packages/mage_windows.tgz + - curl -L -o mage.tgz https://gitlab.irstea.fr/jean-baptiste.faure/mage/-/releases/Test6/downloads/packages/mage_windows.tgz - mkdir -p mage - cd mage - tar xvf ../mage.tgz From e8d0b2c18ffbc17861ae5b11250197ba9449f7f2 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 5 Sep 2023 11:32:31 +0200 Subject: [PATCH 180/289] paper: Add paper draft for SimHydro 2023. --- doc/paper/2023/simhydro/images/BC.png | Bin 0 -> 90046 bytes doc/paper/2023/simhydro/images/apply_sedi.png | Bin 0 -> 105795 bytes doc/paper/2023/simhydro/images/edit_sedi.png | Bin 0 -> 27008 bytes doc/paper/2023/simhydro/images/friction.png | Bin 0 -> 61487 bytes .../2023/simhydro/images/geometry_saar.png | Bin 0 -> 207519 bytes .../simhydro/images/geometry_saar_annoted.png | Bin 0 -> 212669 bytes doc/paper/2023/simhydro/images/init_Saar.png | Bin 0 -> 94077 bytes doc/paper/2023/simhydro/images/network.png | Bin 0 -> 22561 bytes .../2023/simhydro/images/network_saar.png | Bin 0 -> 22284 bytes .../2023/simhydro/images/results_hydro.png | Bin 0 -> 104102 bytes .../2023/simhydro/images/results_sedi.png | Bin 0 -> 92597 bytes doc/paper/2023/simhydro/images/solver_log.png | Bin 0 -> 84044 bytes doc/paper/2023/simhydro/paper.odt | Bin 0 -> 933632 bytes 13 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/paper/2023/simhydro/images/BC.png create mode 100644 doc/paper/2023/simhydro/images/apply_sedi.png create mode 100644 doc/paper/2023/simhydro/images/edit_sedi.png create mode 100644 doc/paper/2023/simhydro/images/friction.png create mode 100644 doc/paper/2023/simhydro/images/geometry_saar.png create mode 100644 doc/paper/2023/simhydro/images/geometry_saar_annoted.png create mode 100644 doc/paper/2023/simhydro/images/init_Saar.png create mode 100644 doc/paper/2023/simhydro/images/network.png create mode 100644 doc/paper/2023/simhydro/images/network_saar.png create mode 100644 doc/paper/2023/simhydro/images/results_hydro.png create mode 100644 doc/paper/2023/simhydro/images/results_sedi.png create mode 100644 doc/paper/2023/simhydro/images/solver_log.png create mode 100644 doc/paper/2023/simhydro/paper.odt diff --git a/doc/paper/2023/simhydro/images/BC.png b/doc/paper/2023/simhydro/images/BC.png new file mode 100644 index 0000000000000000000000000000000000000000..b1137a19f5f52e5467e243d0920d71065197a767 GIT binary patch literal 90046 zcmagGby$__);Ed)Cd=e zddg0C<19qd5T}_S4?}&aqT#0KwR^rd=Y!&~r_h+_W4d}RD%`fR<>DyqClz<+-bjWL z@y^yc?-)jP{>qWfF8lQHWAor39wuh+VBe>I-(`)9j`nTQZ`Nng=O7x2kzEQuI5-%3 zk|va5d*L*GIkMJqKa@0d5nyWmyB#|!k^@d6%5BJSo{5bF}}ZUiB!RGnJ$6{;QOf z={UNWbyq1VsllNkR>B)gNh*JrBkq@>O25lOIbXSGlFg3mHi-N%u-qQ2)Tdb?&o0+0GWtj!H~Sv^F+=DpxeR zaZ)gBAy=W<+u(jE%f9pP`|TkXuyH{h##M}JZE3mmrpn!Z{$24)Wr{DrBTN7TbMpmxw)=6kNCFuzeyMyo24|(8pmz+{ znC43L@YCJFLTR#^{x>x8>HP}#$ZTLqS2>7+{3dF@m6R%K-TL

1GlAN=>M2_Gcru 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

|U8{LkyP$Rh?f%-Dx!=S{Utd^s^q^(R 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

C2z^d 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