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/Model/Geometry/Point.py b/src/Model/Geometry/Point.py
index b3b51e7d..cc1971c0 100644
--- a/src/Model/Geometry/Point.py
+++ b/src/Model/Geometry/Point.py
@@ -19,12 +19,29 @@
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
+
+
+ @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
def name(self):
diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py
index cffcee97..19695c77 100644
--- a/src/Model/Geometry/PointXYZ.py
+++ b/src/Model/Geometry/PointXYZ.py
@@ -26,8 +26,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)
@@ -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)
)
""")
@@ -53,6 +55,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
@@ -63,9 +76,9 @@ 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}"
+ f"WHERE profile = {profile.id}"
)
# Create points list
@@ -79,13 +92,25 @@ class PointXYZ(Point, SQLSubModel):
x = row[2]
y = row[3]
z = row[4]
+ sl = row[5]
new = cls(
name = name,
x = x, y = y, z = z,
+ profile = profile,
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
@@ -94,13 +119,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}" +
+ f"{profile.id}, {sl}" +
")"
)
execute(sql)
@@ -117,7 +144,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/Profile.py b/src/Model/Geometry/Profile.py
index c95d2282..d8137932 100644
--- a/src/Model/Geometry/Profile.py
+++ b/src/Model/Geometry/Profile.py
@@ -50,6 +50,7 @@ class Profile(object):
self._kp = float(kp)
self._name = str(name)
self._reach = reach
+ self._sl = None
self._points: List[Point] = []
@@ -63,6 +64,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
@@ -136,6 +140,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):
+ self._sl = value
+ self._status.modified()
+
@property
def profile_type(self):
"""
@@ -274,6 +291,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/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py
index 9cf15da4..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)
)
""")
@@ -84,6 +86,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
@@ -93,7 +106,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}"
)
@@ -109,6 +122,7 @@ class ProfileXYZ(Profile, SQLSubModel):
num = row[5]
code1 = row[5]
code2 = row[6]
+ sl = row[7]
new = cls(
id=id, num = num,
@@ -118,7 +132,17 @@ class ProfileXYZ(Profile, SQLSubModel):
status = status
)
- data["profile"] = id
+ 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)
profiles[ind] = new
@@ -129,18 +153,20 @@ 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)
- data["profile"] = self.id
+ data["profile"] = self
execute(f"DELETE FROM geometry_pointXYZ WHERE profile = {self.id}")
ind = 0
@@ -178,11 +204,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:
@@ -235,7 +261,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()
@@ -259,7 +285,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()
@@ -272,7 +298,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
diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py
index d2a573f1..0ddf06d8 100644
--- a/src/Model/Geometry/Reach.py
+++ b/src/Model/Geometry/Reach.py
@@ -276,6 +276,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/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/Model/River.py b/src/Model/River.py
index d0ae5b22..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
@@ -31,6 +33,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
@@ -208,6 +211,7 @@ class River(Graph, SQLSubModel):
InitialConditionsDict,
StricklersList,
SolverParametersList,
+ SedimentLayerList,
]
def __init__(self, status=None):
@@ -223,6 +227,7 @@ class River(Graph, SQLSubModel):
self._initial_conditions = InitialConditionsDict(status=self._status)
self._stricklers = StricklersList(status=self._status)
self._parameters = {}
+ self._sediment_layers = SedimentLayerList(status=self._status)
@classmethod
def _sql_create(cls, execute):
@@ -246,6 +251,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,
@@ -289,6 +302,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:
@@ -317,6 +331,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
@@ -366,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/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py
new file mode 100644
index 00000000..638212c8
--- /dev/null
+++ b/src/Model/SedimentLayer/SedimentLayer.py
@@ -0,0 +1,339 @@
+# -*- 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.0, d50 = 0.0, sigma = 0.0,
+ critical_constraint = 0.0,
+ sl = None, status = None):
+ super(Layer, self).__init__()
+
+ self._status = status
+
+ 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
+ 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 = 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("""
+ CREATE TABLE sedimentary_layer_layer(
+ id INTEGER NOT NULL PRIMARY KEY,
+ ind INTEGER NOT NULL,
+ 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)
+ )
+ """)
+
+ return cls._create_submodel(execute)
+
+ @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
+ def _sql_load(cls, execute, data = None):
+ new = []
+ sl = data["sl"]
+
+ table = execute(
+ "SELECT id, ind, name, type, height, d50, sigma, critical_constraint " +
+ "FROM sedimentary_layer_layer " +
+ f"WHERE sl = {sl}"
+ )
+
+ 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],
+ d50 = row[5], sigma = row[6],
+ critical_constraint = row[7],
+ sl = sl, status = data['status']
+ )
+
+ new[ind] = layer
+
+ return new
+
+ def _sql_save(self, execute, data = None):
+ ind = data["ind"]
+ sl = data["sl"]
+
+ sql = (
+ "INSERT INTO " +
+ "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}, " +
+ f"{self._d50}, {self._sigma}, {self._critical_constraint}, " +
+ f"{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
+
+ SedimentLayer._id_cnt = max(id, SedimentLayer._id_cnt+1)
+
+ def __str__(self):
+ s = f"{self.name} ({len(self)})"
+
+ if self.comment != "":
+ s += f" - {self.comment}"
+
+ return s
+
+ 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
+
+ @name.setter
+ 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
+
+ @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):
+ major, minor, release = version.strip().split(".")
+ if major == minor == "0":
+ if int(release) < 2:
+ cls._sql_create(execute)
+
+ 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, 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
+ 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(sl=self, 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 >= 0:
+ next = index - 1
+
+ l = self._layers
+ l[index], l[next] = l[next], l[index]
+
+ self._status.modified()
+
+ def move_down(self, index):
+ if index + 1 < len(self._layers):
+ 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
new file mode 100644
index 00000000..d2b66226
--- /dev/null
+++ b/src/Model/SedimentLayer/SedimentLayerList.py
@@ -0,0 +1,101 @@
+# -*- 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
+
+ # 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)
+
+ return ok
+
+ @property
+ def sediment_layers(self):
+ return self._sl.copy()
+
+ 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/Model/SolverParameters/SolverParametersList.py b/src/Model/SolverParameters/SolverParametersList.py
index a6471356..18c45529 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
@@ -160,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/Model/Study.py b/src/Model/Study.py
index af594c90..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.1"
+ 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/ASolver.py b/src/Solver/ASolver.py
index 64cc01cb..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}")
@@ -213,23 +226,33 @@ 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
- 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"
@@ -242,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"
@@ -262,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"
@@ -287,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,
@@ -318,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)
@@ -330,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 6c129f88..aa68802f 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"),
@@ -97,6 +97,13 @@ class Mage(AbstractSolver):
# Export #
##########
+ def cmd_args(self, study):
+ l = super(Mage, self).cmd_args(study)
+
+ l.append("-r")
+
+ return l
+
def input_param(self):
return "0.REP"
@@ -127,23 +134,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")
@@ -170,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
@@ -182,6 +213,7 @@ class Mage(AbstractSolver):
AVA = []
HYD = []
LIM = []
+ QSO = []
for tab in ["liquid", "solid", "suspenssion"]:
for bound in lst.get_tab(tab):
@@ -191,10 +223,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 +367,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)
@@ -404,6 +447,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 = []
@@ -423,7 +474,6 @@ class Mage8(Mage):
f.write(f"{name} {value}\n")
-
return files
@timer
@@ -459,11 +509,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)
@@ -477,6 +553,7 @@ class Mage8(Mage):
# RESULTS #
###########
+ @timer
def read_bin(self, study, repertory, results, qlog = None):
logger.info(f"read_bin: Start ...")
@@ -597,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
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/src/View/Frictions/Window.py b/src/View/Frictions/Window.py
index 81049593..4af3b66c 100644
--- a/src/View/Frictions/Window.py
+++ b/src/View/Frictions/Window.py
@@ -49,7 +49,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 *
@@ -128,7 +128,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,
@@ -193,7 +193,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 7fb31dae..eb7d5e28 100644
--- a/src/View/Geometry/PlotKPC.py
+++ b/src/View/Geometry/PlotKPZ.py
@@ -29,10 +29,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
@@ -64,7 +64,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
)
@@ -234,7 +234,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 e33b6d54..7776d737 100644
--- a/src/View/Geometry/Window.py
+++ b/src/View/Geometry/Window.py
@@ -38,7 +38,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 ASubMainWindow, WindowToolKit
@@ -230,7 +230,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow):
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
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index 559601c2..2fc14efa 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -18,6 +18,7 @@
import os
import logging
+import subprocess
from queue import Queue
from PyQt5 import QtGui
@@ -49,6 +50,8 @@ from View.LateralContribution.Window import LateralContributionWindow
from View.InitialConditions.Window import InitialConditionsWindow
from View.Stricklers.Window import StricklersWindow
from View.Frictions.Window import FrictionsWindow
+from View.SedimentLayers.Window import SedimentLayersWindow
+from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow
from View.SolverParameters.Window import SolverParametersWindow
from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow
from View.CheckList.Window import CheckListWindow
@@ -87,7 +90,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 = (
@@ -170,6 +174,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_layers,
+ "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers,
## Help
"action_menu_about": self.open_about,
# ToolBar action
@@ -258,15 +264,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)
#########
@@ -617,6 +630,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
else:
params.activateWindow()
+ 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
@@ -683,6 +710,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 #
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/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..3882a7ba 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,38 @@ 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)
+
+ 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)
+
+ 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)
self.redo_sc.activated.connect(self.redo)
@@ -210,22 +244,38 @@ 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)
+
+ 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)
+
+ 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)
+ 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()
+ if self._study.river.has_sediment():
+ self.plot_sed_reach.draw()
+ self.plot_sed_profile.draw()
+
def _set_current_reach(self):
table = self.find(QTableView, f"tableView_reach")
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/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py
new file mode 100644
index 00000000..e48ec5eb
--- /dev/null
+++ b/src/View/SedimentLayers/Edit/Plot.py
@@ -0,0 +1,105 @@
+# -*- 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)
+
+ # 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()
+ names = ["bottom"] + 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(reversed(z_sl)):
+ self.line_kp_sl.append(None)
+ self.line_kp_sl[i], = self.canvas.axes.plot(
+ x, zsl,
+ label=names[-(i+1)],
+ linestyle="solid" if i == len(names) - 1 else "--",
+ lw=1.8,
+ color='grey' if i == len(names) - 1 else None
+ )
+ self.canvas.axes.text(x[0] + 0.01, zsl[0] + 0.01, f'{names[-(i+1)]}')
+
+ self.canvas.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/Table.py b/src/View/SedimentLayers/Edit/Table.py
new file mode 100644
index 00000000..e1a41cb2
--- /dev/null
+++ b/src/View/SedimentLayers/Edit/Table.py
@@ -0,0 +1,183 @@
+# -*- 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
+ 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()
+
+ 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] == "type":
+ self._undo.push(
+ SetTypeCommand(
+ self._sl, row, value
+ )
+ )
+ if self._headers[column] == "height":
+ self._undo.push(
+ SetHeightCommand(
+ 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
+
+ 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.push(
+ MoveCommand(
+ self._sl, "up", row
+ )
+ )
+
+ self.endMoveRows()
+ self.layoutChanged.emit()
+
+ def move_down(self, row, parent=QModelIndex()):
+ if row + 1 >= len(self._sl):
+ return
+
+ target = row
+
+ self.beginMoveRows(parent, row + 1, row + 1, parent, target)
+
+ self._undo.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
new file mode 100644
index 00000000..298fa529
--- /dev/null
+++ b/src/View/SedimentLayers/Edit/UndoCommand.py
@@ -0,0 +1,157 @@
+# -*- 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 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)
+
+ 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 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)
+
+ 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
new file mode 100644
index 00000000..965895d2
--- /dev/null
+++ b/src/View/SedimentLayers/Edit/Window.py
@@ -0,0 +1,170 @@
+# -*- 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.Plot import Plot
+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._set_plot()
+
+ def _set_plot(self):
+ self.plot = Plot(
+ canvas = self.canvas,
+ data = self._sl,
+ 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.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(
+ # 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 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")
+
+ 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
new file mode 100644
index 00000000..fe9e44a9
--- /dev/null
+++ b/src/View/SedimentLayers/Edit/translate.py
@@ -0,0 +1,14 @@
+# -*- 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"),
+ "d50": _translate("Sedimentlayers", "D50"),
+ "sigma": _translate("Sedimentlayers", "Sigma"),
+ "critical_constraint": _translate("Sedimentlayers", "Critical constraint"),
+}
diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py
new file mode 100644
index 00000000..a312a8ce
--- /dev/null
+++ b/src/View/SedimentLayers/Reach/Plot.py
@@ -0,0 +1,102 @@
+# -*- 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)
+ )
+
+ # 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]
+ )
+
+ 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 = 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/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py
new file mode 100644
index 00000000..50b4bba2
--- /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(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 = 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/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py
new file mode 100644
index 00000000..102bec32
--- /dev/null
+++ 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
new file mode 100644
index 00000000..fe3497ae
--- /dev/null
+++ 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
new file mode 100644
index 00000000..d1dd5e69
--- /dev/null
+++ b/src/View/SedimentLayers/Reach/Profile/Window.py
@@ -0,0 +1,161 @@
+# -*- 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.SedimentLayers.Reach.Profile.Plot import Plot
+
+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 - @kp)").replace("@kp", str(self._profile.kp)
+ )
+
+ 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._update_plot()
+
+ def _update_plot(self):
+ self.plot = Plot(
+ canvas = self.canvas,
+ data = self._profile,
+ 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)
+
+ 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(
+ # 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
new file mode 100644
index 00000000..33e204b3
--- /dev/null
+++ 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/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py
new file mode 100644
index 00000000..ce1e9794
--- /dev/null
+++ b/src/View/SedimentLayers/Reach/SLDialog.py
@@ -0,0 +1,61 @@
+# -*- 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):
+ if self.value == _translate("SedimentLayers", "Not defined"):
+ return None
+
+ 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
new file mode 100644
index 00000000..8fdb8d7e
--- /dev/null
+++ b/src/View/SedimentLayers/Reach/Table.py
@@ -0,0 +1,159 @@
+# -*- 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 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()
+
+ 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
new file mode 100644
index 00000000..17e1f0f1
--- /dev/null
+++ b/src/View/SedimentLayers/Reach/UndoCommand.py
@@ -0,0 +1,51 @@
+# -*- 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
+
+
+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
new file mode 100644
index 00000000..ee477edc
--- /dev/null
+++ b/src/View/SedimentLayers/Reach/Window.py
@@ -0,0 +1,190 @@
+# -*- 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.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.SedimentLayers.Window import SedimentLayersWindow
+from View.SedimentLayers.Reach.Profile.Window import ProfileSedimentLayersWindow
+
+_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._update_plot()
+
+ def _update_plot(self):
+ self.plot = Plot(
+ canvas = self.canvas,
+ data = self._reach,
+ toolbar = None,
+ display_current = False
+ )
+ self.plot.draw()
+
+
+ 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)
+ 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(
+ # 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()
+ self._update_plot()
+
+ def redo(self):
+ self._table.redo()
+ self._update_plot()
+
+ 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)
+
+ self._update_plot()
+
+ def edit_profile(self):
+ rows = self.index_selected_rows()
+
+ for row in rows:
+ slw = ProfileSedimentLayersWindow(
+ study = self._study,
+ profile = self._reach.profile(row),
+ parent = self
+ )
+ slw.show()
+
+
+ def edit_sl(self):
+ slw = SedimentLayersWindow(
+ study = self._study,
+ parent = self
+ )
+ slw.show()
diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py
new file mode 100644
index 00000000..f6b17cf8
--- /dev/null
+++ 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/Table.py b/src/View/SedimentLayers/Table.py
new file mode 100644
index 00000000..f375c318
--- /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).comment
+
+ return QVariant()
+
+ def headerData(self, friction, orientation, role):
+ if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
+ return table_headers[self._headers[friction]]
+
+ return 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/UndoCommand.py b/src/View/SedimentLayers/UndoCommand.py
new file mode 100644
index 00000000..f785743b
--- /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):
+ 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..f8b9d2d2
--- /dev/null
+++ b/src/View/SedimentLayers/Window.py
@@ -0,0 +1,170 @@
+# -*- 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.SedimentLayers.Edit.Plot import Plot
+
+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):
+ 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)
+
+ 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)
+
+ 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(
+ # 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.get(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/SolverParameters/translate.py b/src/View/SolverParameters/translate.py
index 76cb70ac..2508629e 100644
--- a/src/View/SolverParameters/translate.py
+++ b/src/View/SolverParameters/translate.py
@@ -55,15 +55,16 @@ 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"),
"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)"),
diff --git a/src/View/ui/EditSedimentLayers.ui b/src/View/ui/EditSedimentLayers.ui
new file mode 100644
index 00000000..6fe4c11b
--- /dev/null
+++ b/src/View/ui/EditSedimentLayers.ui
@@ -0,0 +1,114 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 960
+ 480
+
+
+
+ MainWindow
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 217bd2b4..ea77d698 100644
--- a/src/View/ui/MainWindow.ui
+++ b/src/View/ui/MainWindow.ui
@@ -165,10 +165,18 @@
+
+
@@ -878,6 +886,16 @@
Define 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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..dfe39a82
--- /dev/null
+++ b/src/View/ui/ReachSedimentLayers.ui
@@ -0,0 +1,89 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 960
+ 480
+
+
+
+ MainWindow
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
-
+
+
+ -
+
+
+ Edit sediment layers list
+
+
+
+ -
+
+
+ Apply sediment layers on all reach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+ ressources/edit.pngressources/edit.png
+
+
+ Edit profile
+
+
+ Edit profile sediment layer
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
-
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
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
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
+
-
diff --git a/tests_cases/Saar/Saar.pamhyr b/tests_cases/Saar/Saar.pamhyr
index df56a298..711ef7bc 100644
Binary files a/tests_cases/Saar/Saar.pamhyr and b/tests_cases/Saar/Saar.pamhyr differ