Scenarios: Add window scheme (from copy of network window).

scenarios
Pierre-Antoine Rouby 2024-08-30 10:06:43 +02:00
parent 60ee889794
commit 85d3345043
12 changed files with 1372 additions and 0 deletions

View File

@ -185,3 +185,19 @@ class Scenario(SQLSubModel):
@property
def parent(self):
return self._parent
def __setitem__(self, key, value):
if key == "name":
self.name = value
if key == "description":
self.description = description
def __getitem__(self, key):
if key == "name":
return self.name
if key == "description":
return self.description
if key == "parent":
return self.parent
return None

View File

@ -59,3 +59,12 @@ class Scenarios(PamhyrModelDict):
new = Scenario(parent=parent)
self.set(new._id, new)
return new
@property
def lst(self):
return list(
map(
lambda k: self.get(k),
self._dict
)
)

View File

@ -42,6 +42,7 @@ class Modules(IterableFlag):
# General
STUDY = auto()
CONFIG = auto()
SCENARIOS = auto()
# Modelling
NETWORK = auto()
@ -65,6 +66,7 @@ class Modules(IterableFlag):
def values(cls):
return [
cls.STUDY, cls.CONFIG,
cls.SCENARIOS,
cls.NETWORK,
cls.GEOMETRY,
cls.BOUNDARY_CONDITION,
@ -143,6 +145,7 @@ class Modules(IterableFlag):
_impact = {
Modules.SCENARIOS: Modules.values(),
Modules.NETWORK: [
Modules.GEOMETRY, Modules.BOUNDARY_CONDITION,
Modules.LATERAL_CONTRIBUTION, Modules.FRICTION,

View File

@ -57,6 +57,7 @@ from View.MainWindowTabChecker import WidgetChecker
from View.Configure.Window import ConfigureWindow
from View.Study.Window import NewStudyWindow
from View.About.Window import AboutWindow
from View.Scenarios.Window import ScenariosWindow
from View.Network.Window import NetworkWindow
from View.Geometry.Window import GeometryWindow
from View.BoundaryCondition.Window import BoundaryConditionWindow
@ -109,6 +110,7 @@ define_model_action = [
"action_toolBar_boundary_cond", "action_toolBar_lateral_contrib",
"action_toolBar_frictions", "action_toolBar_initial_cond",
# Menu
"action_menu_edit_scenarios",
"action_menu_run_solver", "action_menu_numerical_parameter",
"action_menu_edit_network", "action_menu_edit_geometry",
"action_menu_boundary_conditions", "action_menu_initial_conditions",
@ -235,6 +237,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
"action_menu_save": self.save_study,
"action_menu_save_as": self.save_as_study,
"action_menu_numerical_parameter": self.open_solver_parameters,
"action_menu_edit_scenarios": self.open_scenarios,
"action_menu_edit_network": self.open_network,
"action_menu_edit_geometry": self.open_geometry,
"action_menu_boundary_conditions": self.open_boundary_cond,
@ -993,6 +996,22 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
self.network = NetworkWindow(study=self._study, parent=self)
self.network.show()
def open_scenarios(self):
"""Open scenarios window
Returns:
Nothing
"""
if self._study is not None:
if self.sub_window_exists(
ScenariosWindow,
data=[self._study, None]
):
return
self.scenarios = ScenariosWindow(study=self._study, parent=self)
self.scenarios.show()
def open_geometry(self):
"""Open geometry window

View File

@ -0,0 +1,112 @@
# ContextMenu.py -- Pamhyr
# Copyright (C) 2024 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtWidgets import (
QMenu,
)
class AbstractMenu(object):
def __init__(self, event=None, pos=None, items=[],
graph=None, trad=None, parent=None):
super(AbstractMenu, self).__init__()
self._menu = QMenu(parent)
self._event = event
self._pos = pos
self._items = items
self._graph = graph
self._trad = trad
self._parent = parent
def map_to_global(self):
return self._parent.mapToGlobal(self._event.pos())
def _exec(self):
return self._menu.exec_(self.map_to_global())
def run(self):
return
class DefaultMenu(AbstractMenu):
def run(self):
add_node = self._menu.addAction(self._trad["menu_add_node"])
action = self._exec()
if action == add_node:
self._parent.add_node(self._pos)
class NodeMenu(AbstractMenu):
def run(self):
item = self._items[0]
node = item.node
delete = self._menu.addAction(self._trad["menu_del_node"])
is_internal = self._graph.is_internal_node(node)
if is_internal:
res = self._graph.reservoir.get_assoc_to_node(node)
if res is not None:
edt_res = self._menu.addAction(
self._trad["menu_edit_res_node"]
)
del_res = self._menu.addAction(
self._trad["menu_del_res_node"]
)
res_edit = True
else:
add_res = self._menu.addAction(self._trad["menu_add_res_node"])
res_edit = False
action = self._exec()
if action == delete:
self._parent.del_node(item)
elif is_internal:
if res_edit:
if action == edt_res:
self._parent.edit_node_reservoir(node)
elif action == del_res:
self._parent.del_node_reservoir(node)
else:
if action == add_res:
self._parent.add_node_reservoir(node)
class EdgeMenu(AbstractMenu):
def run(self):
it = self._items[0]
reverse = self._menu.addAction(self._trad["menu_rev_edge"])
delete = self._menu.addAction(self._trad["menu_del_edge"])
if self._parent.graph.is_enable_edge(it.edge):
enable = self._menu.addAction(self._trad["menu_dis_edge"])
is_enable = True
else:
enable = self._menu.addAction(self._trad["menu_ena_edge"])
is_enable = False
action = self._exec()
if action == delete:
self._parent.del_edge(it)
elif action == enable:
self._parent.enable_edge(it, is_enable)
elif action == reverse:
self._parent.reverse_edge(it)

View File

@ -0,0 +1,813 @@
# GraphWidget.py -- Pamhyr
# Copyright (C) 2023-2024 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import math
import logging
from tools import timer
from PyQt5.QtCore import (
Qt, QPoint, QPointF, QSizeF, QLineF, QRectF,
pyqtSlot, pyqtSignal, QCoreApplication,
)
from PyQt5.QtGui import (
QPainter, QColor, QBrush, QPen, QPainterPath,
QPolygonF,
)
from PyQt5.QtWidgets import (
QApplication, QGraphicsScene, QGraphicsView,
QGraphicsItem, QGraphicsTextItem, QMenu,
)
from View.Scenarios.UndoCommand import *
from View.Scenarios.ContextMenu import (
DefaultMenu, NodeMenu, EdgeMenu,
)
logger = logging.getLogger()
_translate = QCoreApplication.translate
class ScenarioItem(QGraphicsItem):
Type = QGraphicsItem.UserType + 1
def __init__(self, node, graph_widget):
super(ScenarioItem, self).__init__()
self.node = node
self.setPos(QPointF(self.node.pos.x, self.node.pos.y))
self.graph = graph_widget
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
self.setZValue(1)
def type(self):
return NodeItem.Type
def boundingRect(self):
adjust = 2.0
return QRectF(-10 - adjust, -10 - adjust, 20 + adjust, 20 + adjust)
def shape(self):
path = QPainterPath()
if self.graph.graph._reservoir.get_assoc_to_node(self.node) is None:
path.addEllipse(-10, -10, 20, 20)
else:
path.addRect(-10, -10, 20, 20)
return path
@timer
def paint(self, painter, option, widget):
painter.setPen(Qt.NoPen)
# Select color in function of node position in graph and
# status
color = Qt.darkBlue
if self.graph.selected_new_edge_src_node() == self:
color = Qt.darkRed
elif self.graph.selected_item() == self:
color = Qt.red
elif not self.graph.graph.is_enable_node(self.node):
color = Qt.darkGray
elif self.graph.graph.is_upstream_node(self.node):
color = Qt.yellow
elif self.graph.graph.is_downstream_node(self.node):
color = Qt.green
painter.setBrush(QBrush(color))
if self.graph.graph._reservoir.get_assoc_to_node(self.node) is None:
painter.drawEllipse(-10, -10, 20, 20)
else:
painter.drawRect(-10, -10, 20, 20)
def itemChange(self, change, value):
if change == QGraphicsItem.ItemPositionHasChanged:
self.graph.node_change_position(value, self)
self.graph.update_edges(self)
return super(NodeItem, self).itemChange(change, value)
def mousePressEvent(self, event):
self.update()
if not self.graph._only_display:
super(NodeItem, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
self.update()
super(NodeItem, self).mouseReleaseEvent(event)
def mouseMoveEvent(self, event):
self.update()
if not self.graph._only_display:
super(NodeItem, self).mouseMoveEvent(event)
class EdgeItem(QGraphicsItem):
_arc_dist = 0.1
Type = QGraphicsItem.UserType + 2
def __init__(self, src_node_item, dest_node_item,
edge, graph_widget):
super(EdgeItem, self).__init__()
self.src_node = src_node_item
self.dest_node = dest_node_item
self.edge = edge
self.graph = graph_widget
self._ind = 1
geometry = self.compute_arc_two_point(
QPointF(
self.src_node.pos().x(),
self.src_node.pos().y()
),
QPointF(
self.dest_node.pos().x(),
self.dest_node.pos().y()
),
)
self._bound_rect = geometry['bound_rect']
self._arc = geometry['arc']
self.setAcceptedMouseButtons(Qt.NoButton)
def type(self):
return Edge.Type
def boundingRect(self):
# Rectangle of edge for display update
pen_width = 2.0
extra = 0 # (pen_width + 5) / 2.0
return self._bound_rect.normalized().adjusted(
-extra, -extra, extra, extra
)
def shape(self):
return self._arc
def paint(self, painter, option, widget):
# self.paint_line(painter, option, widget)
self.paint_arc(painter, option, widget)
@timer
def paint_line(self, painter, option, widget):
# Draw shape of the edge
# color = QColor(Qt.white)
# color.setAlpha(128)
# painter.setBrush(color)
# painter.drawPath(self.shape())
line = QLineF(self.src_node.pos(), self.dest_node.pos())
if line.length() == 0.0:
return
# Select color
# color = Qt.darkBlue
color = Qt.black
if self.graph.selected_item() == self:
color = Qt.red
elif self.graph.current_edge() == self:
color = Qt.blue
elif not self.graph.graph.is_enable_edge(self.edge):
color = Qt.darkGray
painter.setPen(
QPen(
color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin
)
)
painter.drawLine(line)
# Draw arrow
center = QPointF(
(self.src_node.pos().x() + self.dest_node.pos().x()) / 2,
(self.src_node.pos().y() + self.dest_node.pos().y()) / 2,
)
angle = math.acos(line.dx() / line.length())
if line.dy() >= 0:
angle = (math.pi * 2.0) - angle
self.paint_arrow(
painter, option, widget,
color, center, angle
)
@timer
def paint_arc(self, painter, option, widget):
line = QLineF(self.src_node.pos(), self.dest_node.pos())
if line.length() == 0.0:
return
# Select color
# color = Qt.darkBlue
color = Qt.black
if self.graph.selected_item() == self:
color = Qt.red
elif self.graph.current_edge() == self:
color = Qt.blue
elif not self.graph.graph.is_enable_edge(self.edge):
color = Qt.darkGray
painter.setPen(
QPen(
color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin
)
)
geometry = self.compute_arc_two_point(
QPointF(
self.src_node.pos().x(),
self.src_node.pos().y()
),
QPointF(
self.dest_node.pos().x(),
self.dest_node.pos().y()
),
)
self._bound_rect = geometry['bound_rect']
self._arc = geometry['arc']
painter.drawPath(self._arc)
line = geometry['line']
angle = math.acos(line.dx() / line.length())
if line.dy() >= 0:
angle = (math.pi * 2.0) - angle
self.paint_arrow(
painter, option, widget,
color, geometry['c'], angle,
)
def compute_arc_two_point(self, a, b):
# This function is a black magic spell for invoking an arc
# path between two points in Qt! (In fact, it's just cryptic
# trigonometry functions from stackoverflow:
# https://stackoverflow.com/questions/26901540/arc-in-qgraphicsscene)
c = self.compute_arc_p3(a, b)
line_ac = QLineF(a, c)
line_bc = QLineF(b, c)
line_ab = QLineF(a, b)
rad = abs(
line_bc.length() / (
2 * math.sin(
math.radians(
line_ac.angleTo(line_ab)
)
)
)
)
bisector_bc = QLineF(line_bc.pointAt(0.5), line_bc.p2())
bisector_bc.setAngle(line_bc.normalVector().angle())
bisector_ab = QLineF(line_ab.pointAt(0.5), line_ab.p2())
bisector_ab.setAngle(line_ab.normalVector().angle())
center = QPointF(0, 0)
bisector_ab.intersect(bisector_bc, center)
circle = QRectF(
center.x() - rad,
center.y() - rad,
rad * 2, rad * 2
)
line_oa = QLineF(center, a)
line_ob = QLineF(center, b)
line_oc = QLineF(center, c)
start = line_ob
end = line_oa
w = 1
bounding_rect = circle.adjusted(
-w, -w, w, w
)
path = QPainterPath()
path.arcMoveTo(circle, start.angle())
path.arcTo(circle, start.angle(), start.angleTo(end))
return {
'bound_rect': bounding_rect,
'arc': path,
'c': c,
'line': line_ab,
}
def compute_arc_p3(self, p1, p2):
dist_p3 = self._ind * self._arc_dist
center_p1_p2 = QPointF(
(p1.x() + p2.x()) / 2,
(p1.y() + p2.y()) / 2
)
# u = (p2.x() - p1.x(), p2.y() - p1.y())
v = (p2.y() - p1.y(), p1.x() - p2.x())
p3 = QPointF(
center_p1_p2.x() + dist_p3 * v[0],
center_p1_p2.y() + dist_p3 * v[1]
)
return p3
def paint_arrow(self, painter, option, widget,
color, center, angle):
brush = QBrush()
brush.setColor(color)
brush.setStyle(Qt.SolidPattern)
size = 10.0
arrow_p1 = center + QPointF(
math.sin(angle - math.pi / 3) * size,
math.cos(angle - math.pi / 3) * size
)
arrow_p2 = center + QPointF(
math.sin(angle - math.pi + math.pi / 3) * size,
math.cos(angle - math.pi + math.pi / 3) * size
)
poly = QPolygonF([center, arrow_p1, arrow_p2])
path = QPainterPath()
path.addPolygon(poly)
painter.drawPolygon(poly)
painter.fillPath(path, brush)
class NodeText(QGraphicsTextItem):
def __init__(self, node_item):
super(NodeText, self).__init__()
self.item = node_item
self.setPlainText(self.item.node.name)
self.setDefaultTextColor(Qt.black)
self.set_custom_pos(self.item.pos())
self.setZValue(2)
def set_custom_pos(self, pos):
x = pos.x()
y = pos.y() - 42 # Dont panic! The answer is 42
self.setPos(QPointF(x, y))
@timer
def paint(self, painter, option, widget):
color = QColor(Qt.white)
color.setAlpha(128)
# Draw text background
painter.setBrush(color)
painter.drawRect(self.boundingRect())
# Draw text
super(NodeText, self).paint(painter, option, widget)
def rename(self):
# Update the node text
self.setPlainText(self.item.node.name)
class GraphWidget(QGraphicsView):
changeEdge = pyqtSignal(object)
changeNode = pyqtSignal(object)
def __init__(self, graph, parent=None,
min_size=(400, 400), max_size=None,
size=None, only_display=False, undo=None,
trad=None):
super(GraphWidget, self).__init__(parent=parent)
self.parent = parent
self._state = "move"
self._only_display = only_display
self._undo = undo
self._trad = trad
self.graph = graph
self._selected_item = None
self._selected_new_edge_src_node = None
self._current_edge = None
self._current_moved_node = None
self.tmp_line = None
self.node_items = []
self.edge_items = []
self.texts = {}
self.m_origin_x = 0.0
self.m_origin_y = 0.0
self.clicked = False
self.setup_scene(min_size, max_size, size)
def setup_scene(self, min_size, max_size, size):
scene = QGraphicsScene(self)
scene.setItemIndexMethod(QGraphicsScene.NoIndex)
scene.setSceneRect(0, 0, 2000, 2000)
self.setScene(scene)
self.setCacheMode(QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
self.setRenderHint(QPainter.Antialiasing)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorViewCenter)
self.scale(1, 1)
self.previousScale = 1
if min_size:
self.setMinimumSize(*min_size)
if max_size:
self.setMaximumSize(*max_size)
if size:
self.resize(*size)
self.create_items()
@timer
def create_items(self):
"""Create all items and draw its
Returns:
Nothing
"""
self.node_items = []
self.edge_items = []
self.texts = {}
for node in self.graph.nodes():
inode = NodeItem(node, self)
self.texts[inode] = NodeText(inode)
self.scene().addItem(self.texts[inode])
self.scene().addItem(inode)
self.node_items.append(inode)
curr_edge = self.graph.current_reach()
iedges = []
multiple_edges = {}
for edge in self.graph.edges():
n1 = next(
filter(
lambda n: n.node.name == edge.node1.name,
self.node_items
)
)
n2 = next(
filter(
lambda n: n.node.name == edge.node2.name,
self.node_items
)
)
# Multiple edges counter
if (n1, n2) not in multiple_edges:
ind = 1
else:
ind = multiple_edges[(n1, n2)] + 1
multiple_edges[(n1, n2)] = ind
iedge = EdgeItem(n1, n2, edge, self)
iedge._ind = ind
if edge == curr_edge:
self._current_edge = iedge
iedges.append(iedge)
for iedge in reversed(iedges):
self.scene().addItem(iedge)
self.edge_items.append(iedge)
def state(self, status):
"""Set the current status of draw widget
Set the current status of draw widget, like "move", "add" or
"del"
Args:
status: String of current status
Returns:
Nothing
"""
self._state = status
def add_node(self, pos):
"""Create a new node in graph and draw it
Args:
pos: The position of new node
Returns:
Nothing
"""
node = self.graph.create_node(pos.x(), pos.y())
self._undo.push(
AddNodeCommand(
self.graph, node
)
)
inode = NodeItem(node, self)
self.scene().addItem(inode)
self.node_items.append(inode)
self.texts[inode] = NodeText(inode)
self.scene().addItem(self.texts[inode])
self.changeNode.emit(self.sender())
def del_node(self, item):
"""Delete a node and update display
Args:
item: The node item to delete
Returns:
Nothing
"""
node = item.node
self._undo.push(
DelNodeCommand(
self.graph, node
)
)
self.scene().clear()
self.create_items()
self.changeNode.emit(self.sender())
self.changeEdge.emit(self.sender())
def rename_nodes(self):
"""Update all nodes item name in scene
Returns:
Nothing
"""
for i in self.texts:
if type(i) is NodeItem:
self.texts[i].rename()
def add_node_reservoir(self, node):
self.parent.add_node_reservoir(node)
self.display_update()
def del_node_reservoir(self, node):
self.parent.del_node_reservoir(node)
self.changeNode.emit(self.sender())
self.display_update()
def edit_node_reservoir(self, node):
self.changeNode.emit(self.sender())
self.parent.edit_node_reservoir(node)
def enable_edge(self, edge, prev):
self._undo.push(
EnableEdgeCommand(
edge.edge, not prev
)
)
self.changeEdge.emit(self.sender())
self.changeNode.emit(self.sender())
self.display_update()
def reverse_edge(self, edge):
self._undo.push(
ReverseEdgeCommand(
edge.edge
)
)
self.changeEdge.emit(self.sender())
self.display_update()
def display_update(self):
"""Clear the scene and redraw it
Returns:
Nothing
"""
self.scene().clear()
self.create_items()
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_Plus:
self.scaleView(1.2)
elif key == Qt.Key_Minus:
self.scaleView(1 / 1.2)
elif key == Qt.Key_Escape:
self.reset_selection()
else:
super(GraphWidget, self).keyPressEvent(event)
def drawBackground(self, painter, rect):
sceneRect = self.sceneRect()
painter.fillRect(
rect.intersected(sceneRect),
QBrush(Qt.lightGray)
)
painter.setBrush(Qt.NoBrush)
painter.drawRect(sceneRect)
def wheelEvent(self, event):
self.scaleView(math.pow(2.0, -event.angleDelta().y() / 240.0))
def scaleView(self, scaleFactor):
factor = self.transform().scale(
scaleFactor, scaleFactor
).mapRect(QRectF(0, 0, 1, 1)).width()
if factor < 0.05 or factor > 1.5:
return
self.scale(scaleFactor, scaleFactor)
def mousePressEvent(self, event):
pos = self.mapToScene(event.pos())
self.clicked = True
if event.buttons() & Qt.RightButton:
self.update()
super(GraphWidget, self).mousePressEvent(event)
return
# Move item and select edge item
if self._state == "move":
self._selected_new_edge_src_node = None
items = self.items(event.pos())
if items and type(items[0]) is EdgeItem:
edge = items[0]
if edge:
self.set_current_edge(edge)
elif items and type(items[0]) is NodeItem:
self._mouse_origin_x = pos.x()
self._mouse_origin_y = pos.y()
self._current_moved_node = items[0]
# Add nodes and edges
elif self._state == "add":
items = self.items(event.pos())
nodes = list(filter(lambda i: type(i) is NodeItem, items))
if not nodes:
self.add_node(pos)
self.set_selected_new_edge_src_node(None)
else:
if self.selected_new_edge_src_node() is None:
self.set_selected_new_edge_src_node(nodes[0])
else:
self.add_edge(self.selected_new_edge_src_node(), nodes[0])
# Delete nodes and edges
elif self._state == "del":
self._selected_new_edge_src_node = None
items = list(
filter(
lambda i: type(i) is NodeItem or type(i) is EdgeItem,
self.items(event.pos())
)
)
if len(items) > 0:
item = items[0]
if type(item) is NodeItem:
self.del_node(item)
elif type(item) is EdgeItem:
self.del_edge(item)
self.update()
super(GraphWidget, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
self.clicked = False
if self._only_display:
return
if self._state == "move":
if self._current_moved_node is not None:
pos = self.mapToScene(event.pos())
self._undo.push(
SetNodePosCommand(
self._current_moved_node,
(pos.x(), pos.y()),
(self._mouse_origin_x,
self._mouse_origin_y)
)
)
self.update()
super(GraphWidget, self).mouseReleaseEvent(event)
def mouseMoveEvent(self, event):
pos = self.mapToScene(event.pos())
# Selecte item on the fly
items = self.items(event.pos())
selectable_items = list(
filter(
lambda i: (type(i) is NodeItem or type(i) is EdgeItem),
items
)
)
if selectable_items:
self.set_selected_item(selectable_items[0])
elif not self.clicked:
self.set_selected_item(None)
# Update temporary line
if self.selected_new_edge_src_node() is not None:
if self.tmp_line is None:
self.tmp_line = NewEdgeLine(
self.selected_new_edge_src_node().pos(),
pos
)
self.scene().addItem(self.tmp_line)
else:
self.tmp_line.dest = pos
self.tmp_line.update()
# If state is "move"
if self._state == "move":
# Move on scene
if not self.selected_item():
if event.buttons() & Qt.LeftButton:
old_p = self.mapToScene(
int(self.m_origin_x), int(self.m_origin_y)
)
new_p = self.mapToScene(event.pos())
translation = new_p - old_p
self.translate(translation.x(), translation.y())
self.m_origin_x = event.x()
self.m_origin_y = event.y()
# Propagate event
self.update()
super(GraphWidget, self).mouseMoveEvent(event)
# Contextual menu
def contextMenuEvent(self, event):
if self._only_display:
return
pos = self.mapToScene(event.pos())
items = self.items(event.pos())
# Select current menu
while len(items) > 0:
if type(items[0]) in [NodeItem, EdgeItem]:
break
else:
items = items[1:]
if len(items) == 0:
m_type = DefaultMenu
elif type(items[0]) is NodeItem:
m_type = NodeMenu
elif type(items[0]) is EdgeItem:
m_type = EdgeMenu
else:
m_type = DefaultMenu
# Create and exec menu
m = m_type(
event=event, pos=pos, items=items,
graph=self.graph, trad=self._trad, parent=self
)
m.run()
self.clicked = False

View File

@ -0,0 +1,82 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2024 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import logger_exception
from View.Scenarios.GraphWidget import GraphWidget
from View.Scenarios.UndoCommand import *
from View.Tools.PamhyrTable import PamhyrTableModel
from PyQt5.QtCore import (
Qt, QRect, QVariant, QAbstractTableModel, pyqtSlot, pyqtSignal,
QEvent,
)
from PyQt5.QtWidgets import (
QTableView, QItemDelegate, QComboBox, QLineEdit, QHBoxLayout, QSlider,
QPushButton, QCheckBox, QStyledItemDelegate, QStyleOptionButton, QStyle,
QApplication,
)
logger = logging.getLogger()
class ScenariosTableModel(PamhyrTableModel):
def _setup_lst(self):
self._lst = self._data.scenarios.lst
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
if self._headers[index.column()] == "parent":
parent = self._lst[index.row()][self._headers[index.column()]]
if parent is None:
return ""
return parent.name
return self._lst[index.row()][self._headers[index.column()]]
@pyqtSlot()
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
if role == Qt.EditRole:
try:
self._undo.push(
SetCommand(
self._lst[index.row()],
self._headers[index.column()],
value
)
)
except Exception as e:
logger_exception(e)
self.layoutChanged.emit()
return True
self.dataChanged.emit(index, index)
def update(self):
self.layoutChanged.emit()

View File

@ -0,0 +1,68 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2024 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QUndoCommand, QUndoStack,
)
class AddScenariosCommand(QUndoCommand):
def __init__(self, study, source):
QUndoCommand.__init__(self)
self._study = study
self._source = source
def undo(self):
logger.error(f"TODO {type(self)} : undo")
def redo(self):
self._study.new_scenario_from_current(switch=False)
class DelScenariosCommand(QUndoCommand):
def __init__(self, study, scenario):
QUndoCommand.__init__(self)
self._study = study
self._scenario = scenario
def undo(self):
logger.error(f"TODO {type(self)} : undo")
def redo(self):
logger.error(f"TODO {type(self)} : redo")
class SetCommand(QUndoCommand):
def __init__(self, scenario, column, new_value):
QUndoCommand.__init__(self)
self._el = scenario
self._column = column
self._old = self._el[self._column]
self._new = new_value
def undo(self):
self._el[self._column] = self._old
def redo(self):
self._el[self._column] = self._new

View File

@ -0,0 +1,131 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2024 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5.QtCore import (
Qt, QRect, QVariant, QAbstractTableModel, pyqtSlot, pyqtSignal,
QEvent,
)
from PyQt5.QtWidgets import (
QTableView, QItemDelegate, QComboBox, QLineEdit, QHBoxLayout, QSlider,
QPushButton, QCheckBox, QStyledItemDelegate, QStyleOptionButton, QStyle,
QApplication, QToolBar, QAction, QHeaderView, QAbstractItemView,
QUndoStack, QShortcut,
)
from Modules import Modules
from Model.River import RiverNode, RiverReach, River
from View.Tools.PamhyrWindow import PamhyrWindow
from View.Scenarios.GraphWidget import GraphWidget
from View.Scenarios.UndoCommand import *
from View.Scenarios.translate import ScenariosTranslate
from View.Scenarios.Table import ScenariosTableModel
# Reservoir short cut
from View.Reservoir.Edit.Window import EditReservoirWindow
import View.Reservoir.UndoCommand as ResUndoCommand
logger = logging.getLogger()
_translate = QCoreApplication.translate
class ScenariosWindow(PamhyrWindow):
_pamhyr_ui = "Scenarios"
_pamhyr_name = "Scenarios"
def __init__(self, study=None, config=None, parent=None):
trad = ScenariosTranslate()
name = trad[self._pamhyr_name] + " - " + study.name
super(ScenariosWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
options=['undo'],
parent=parent,
)
self._table_headers_scenarios = self._trad.get_dict(
"table_headers_scenarios"
)
self.setup_graph()
self.setup_table()
self.setup_connections()
def setup_graph(self):
# self._graph_widget = GraphWidget(
# self._study,
# undo=self._undo_stack,
# trad=self._trad,
# parent=self,
# )
self._graph_layout = self.find(
QHBoxLayout, "horizontalLayout_graph"
)
# self._graph_layout.addWidget(self._graph_widget)
def setup_table(self):
table = self.find(QTableView, "tableView_scenarios")
self._scenarios_model = ScenariosTableModel(
table_view=table,
table_headers=self._table_headers_scenarios,
editable_headers=["name", "description"],
data=self._study,
undo=self._undo_stack,
)
table.setModel(self._scenarios_model)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader()\
.setSectionResizeMode(QHeaderView.Stretch)
def setup_connections(self):
self._scenarios_model.dataChanged.connect(self.update)
# self._scenarios_model.dataChanged.connect(
# self._graph_widget.rename_scenarios
# )
# self._graph_widget.changeScenarios.connect(self.update)
def _undo(self):
self._undo_stack.undo()
self.update()
def _redo(self):
self._undo_stack.redo()
self.update()
def update(self):
self._scenarios_model.update()
self._graph_widget.display_update()
self._propagate_update(key=Modules.SCENARIOS)

View File

@ -0,0 +1,45 @@
# translate.py -- Pamhyr
# Copyright (C) 2023-2024 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
class ScenariosTranslate(MainTranslate):
def __init__(self):
super(ScenariosTranslate, self).__init__()
self._dict["Scenarios"] = _translate(
"Scenarios", "Scenarios"
)
self._dict["menu_del_scenario"] = _translate(
"Scenarios", "Delete the scenario"
)
self._dict["menu_add_scenario"] = _translate(
"Scenarios", "Create new derived scenario"
)
self._sub_dict["table_headers_scenarios"] = {
"name": self._dict['name'],
"description": self._dict['description'],
"parent": _translate("Scenarios", "Parent"),
}

View File

@ -210,7 +210,14 @@
<addaction name="action_menu_additional_file"/>
<addaction name="menuMage"/>
</widget>
<widget class="QMenu" name="menu_scenarios">
<property name="title">
<string>Scenarios</string>
</property>
<addaction name="action_menu_edit_scenarios"/>
</widget>
<addaction name="menu_File"/>
<addaction name="menu_scenarios"/>
<addaction name="menu_network"/>
<addaction name="menu_geometry"/>
<addaction name="menu_Hydraulics"/>
@ -730,6 +737,11 @@
<string>REP additional lines</string>
</property>
</action>
<action name="action_menu_edit_scenarios">
<property name="text">
<string>Edit scenarios tree</string>
</property>
</action>
</widget>
<resources/>
<connections>

62
src/View/ui/Scenarios.ui Normal file
View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>990</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QWidget" name="layoutWidget">
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_graph"/>
</item>
</layout>
</widget>
<widget class="QTableView" name="tableView_scenarios">
<property name="minimumSize">
<size>
<width>650</width>
<height>150</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>990</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>