mirror of https://gitlab.com/pamhyr/pamhyr2
Scenarios: Add window scheme (from copy of network window).
parent
60ee889794
commit
85d3345043
|
|
@ -185,3 +185,19 @@ class Scenario(SQLSubModel):
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
return self._parent
|
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
|
||||||
|
|
|
||||||
|
|
@ -59,3 +59,12 @@ class Scenarios(PamhyrModelDict):
|
||||||
new = Scenario(parent=parent)
|
new = Scenario(parent=parent)
|
||||||
self.set(new._id, new)
|
self.set(new._id, new)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lst(self):
|
||||||
|
return list(
|
||||||
|
map(
|
||||||
|
lambda k: self.get(k),
|
||||||
|
self._dict
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ class Modules(IterableFlag):
|
||||||
# General
|
# General
|
||||||
STUDY = auto()
|
STUDY = auto()
|
||||||
CONFIG = auto()
|
CONFIG = auto()
|
||||||
|
SCENARIOS = auto()
|
||||||
|
|
||||||
# Modelling
|
# Modelling
|
||||||
NETWORK = auto()
|
NETWORK = auto()
|
||||||
|
|
@ -65,6 +66,7 @@ class Modules(IterableFlag):
|
||||||
def values(cls):
|
def values(cls):
|
||||||
return [
|
return [
|
||||||
cls.STUDY, cls.CONFIG,
|
cls.STUDY, cls.CONFIG,
|
||||||
|
cls.SCENARIOS,
|
||||||
cls.NETWORK,
|
cls.NETWORK,
|
||||||
cls.GEOMETRY,
|
cls.GEOMETRY,
|
||||||
cls.BOUNDARY_CONDITION,
|
cls.BOUNDARY_CONDITION,
|
||||||
|
|
@ -143,6 +145,7 @@ class Modules(IterableFlag):
|
||||||
|
|
||||||
|
|
||||||
_impact = {
|
_impact = {
|
||||||
|
Modules.SCENARIOS: Modules.values(),
|
||||||
Modules.NETWORK: [
|
Modules.NETWORK: [
|
||||||
Modules.GEOMETRY, Modules.BOUNDARY_CONDITION,
|
Modules.GEOMETRY, Modules.BOUNDARY_CONDITION,
|
||||||
Modules.LATERAL_CONTRIBUTION, Modules.FRICTION,
|
Modules.LATERAL_CONTRIBUTION, Modules.FRICTION,
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ from View.MainWindowTabChecker import WidgetChecker
|
||||||
from View.Configure.Window import ConfigureWindow
|
from View.Configure.Window import ConfigureWindow
|
||||||
from View.Study.Window import NewStudyWindow
|
from View.Study.Window import NewStudyWindow
|
||||||
from View.About.Window import AboutWindow
|
from View.About.Window import AboutWindow
|
||||||
|
from View.Scenarios.Window import ScenariosWindow
|
||||||
from View.Network.Window import NetworkWindow
|
from View.Network.Window import NetworkWindow
|
||||||
from View.Geometry.Window import GeometryWindow
|
from View.Geometry.Window import GeometryWindow
|
||||||
from View.BoundaryCondition.Window import BoundaryConditionWindow
|
from View.BoundaryCondition.Window import BoundaryConditionWindow
|
||||||
|
|
@ -109,6 +110,7 @@ define_model_action = [
|
||||||
"action_toolBar_boundary_cond", "action_toolBar_lateral_contrib",
|
"action_toolBar_boundary_cond", "action_toolBar_lateral_contrib",
|
||||||
"action_toolBar_frictions", "action_toolBar_initial_cond",
|
"action_toolBar_frictions", "action_toolBar_initial_cond",
|
||||||
# Menu
|
# Menu
|
||||||
|
"action_menu_edit_scenarios",
|
||||||
"action_menu_run_solver", "action_menu_numerical_parameter",
|
"action_menu_run_solver", "action_menu_numerical_parameter",
|
||||||
"action_menu_edit_network", "action_menu_edit_geometry",
|
"action_menu_edit_network", "action_menu_edit_geometry",
|
||||||
"action_menu_boundary_conditions", "action_menu_initial_conditions",
|
"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": self.save_study,
|
||||||
"action_menu_save_as": self.save_as_study,
|
"action_menu_save_as": self.save_as_study,
|
||||||
"action_menu_numerical_parameter": self.open_solver_parameters,
|
"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_network": self.open_network,
|
||||||
"action_menu_edit_geometry": self.open_geometry,
|
"action_menu_edit_geometry": self.open_geometry,
|
||||||
"action_menu_boundary_conditions": self.open_boundary_cond,
|
"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 = NetworkWindow(study=self._study, parent=self)
|
||||||
self.network.show()
|
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):
|
def open_geometry(self):
|
||||||
"""Open geometry window
|
"""Open geometry window
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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"),
|
||||||
|
}
|
||||||
|
|
@ -210,7 +210,14 @@
|
||||||
<addaction name="action_menu_additional_file"/>
|
<addaction name="action_menu_additional_file"/>
|
||||||
<addaction name="menuMage"/>
|
<addaction name="menuMage"/>
|
||||||
</widget>
|
</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_File"/>
|
||||||
|
<addaction name="menu_scenarios"/>
|
||||||
<addaction name="menu_network"/>
|
<addaction name="menu_network"/>
|
||||||
<addaction name="menu_geometry"/>
|
<addaction name="menu_geometry"/>
|
||||||
<addaction name="menu_Hydraulics"/>
|
<addaction name="menu_Hydraulics"/>
|
||||||
|
|
@ -730,6 +737,11 @@
|
||||||
<string>REP additional lines</string>
|
<string>REP additional lines</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_menu_edit_scenarios">
|
||||||
|
<property name="text">
|
||||||
|
<string>Edit scenarios tree</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue