diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 91a48e2c1909f60f64dc09814219e21b0e93e832..94768e8a97e954f3c0bf5f44962ddd15b8b82dd6 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -637,7 +637,7 @@ class AddNodeCommand(QUndoCommand): self._node = node def undo(self): - self._graph.remove_node(self._node.name) + self._graph.remove_node(self._node) def redo(self): self._graph.insert_node(self._node) diff --git a/src/Model/Network/Graph.py b/src/Model/Network/Graph.py index df48f03da6932229d9365347c7f4abebe09903a1..69ded8450cb8b9b388c9d8d41d7f34ad13026fe8 100644 --- a/src/Model/Network/Graph.py +++ b/src/Model/Network/Graph.py @@ -142,26 +142,21 @@ class Graph(object): def insert_node(self, node): return self._add_node(node) - def remove_node(self, node_name: str): - self._nodes = list( - filter( - lambda n: n.name != node_name, - self._nodes - ) - ) - self._remove_associated_edge(node_name) + def remove_node(self, node): + self._nodes.remove(node) + self._remove_associated_edge(node) self._status.modified() - def _remove_associated_edge(self, node_name: str): + def _remove_associated_edge(self, node: str): edges = list( filter( - lambda e: (e.node1.name == node_name or - e.node2.name == node_name), + lambda e: (e.node1 == node or + e.node2 == node), self._edges, ) ) for edge in edges: - self.remove_edge(edge.name) + self.remove_edge(edge) def create_node(self, x: float = 0.0, y: float = 0.0): node = self._create_node(x, y) @@ -176,11 +171,11 @@ class Graph(object): return edge def _add_edge(self, edge): - # This edge already exists ? - if any(filter(lambda e: (e.node1 == edge.node1 and - e.node2 == edge.node2), - self._edges)): - return None + # # This edge already exists ? + # if any(filter(lambda e: (e.node1 == edge.node1 and + # e.node2 == edge.node2), + # self._edges)): + # return None self._edges.append(edge) @@ -197,13 +192,8 @@ class Graph(object): def create_edge(self, n1: Node, n2: Node): return self._create_edge(n1, n2) - def remove_edge(self, edge_name: str): - self._edges = list( - filter( - lambda e: e.name != edge_name, - self._edges - ) - ) + def remove_edge(self, edge): + self._edges.remove(edge) self._status.modified() def is_upstream_node(self, node): diff --git a/src/Model/Network/test_network.py b/src/Model/Network/test_network.py index e9b5c428e1a9b87484d711d4ef27f825d86c1f87..23cbdcceb05bb012259f01f675eb4a817140be57 100644 --- a/src/Model/Network/test_network.py +++ b/src/Model/Network/test_network.py @@ -60,7 +60,7 @@ class GraphTestCase(unittest.TestCase): e0 = g.add_edge(n0, n1) - g.remove_edge(e0.name) + g.remove_edge(e0) self.assertEqual(g.nodes_counts(), 2) self.assertEqual(g.edges_counts(), 0) @@ -73,7 +73,7 @@ class GraphTestCase(unittest.TestCase): e0 = g.add_edge(n0, n1) - g.remove_node(n0.name) + g.remove_node(n0) self.assertEqual(g.nodes_counts(), 1) self.assertEqual(g.edges_counts(), 0) @@ -88,7 +88,7 @@ class GraphTestCase(unittest.TestCase): e0 = g.add_edge(n0, n1) e1 = g.add_edge(n1, n2) - g.remove_node(n1.name) + g.remove_node(n1) self.assertEqual(g.nodes_counts(), 2) self.assertEqual(g.edges_counts(), 0) diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index edc6744259d1ccbdaa5d76c557f67076e6019659..319a4f9e0b5982212ee2e90e156c2febc6c01b7a 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -120,6 +120,8 @@ class NodeItem(QGraphicsItem): class EdgeItem(QGraphicsItem): + _arc_dist = 0.1 + Type = QGraphicsItem.UserType + 2 def __init__(self, src_node_item, dest_node_item, @@ -131,6 +133,21 @@ class EdgeItem(QGraphicsItem): 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) @@ -140,32 +157,21 @@ class EdgeItem(QGraphicsItem): def boundingRect(self): # Rectangle of edge for display update pen_width = 2.0 - extra = (pen_width + 5) / 2.0 + extra = 0 #(pen_width + 5) / 2.0 - return QRectF( - self.src_node.pos(), - QSizeF( - self.dest_node.pos().x() - self.src_node.pos().x(), - self.dest_node.pos().y() - self.src_node.pos().y() - ) - ).normalized().adjusted(-extra, -extra, extra, extra) + return self._bound_rect.normalized().adjusted( + -extra, -extra, extra, extra + ) def shape(self): - # Shape around edge for mouse selection - vec = self.dest_node.pos() - self.src_node.pos() - vec = vec * (5 / math.sqrt(QPointF.dotProduct(vec, vec))) - extra = QPointF(vec.y(), -vec.x()) - - result = QPainterPath(self.src_node.pos() - vec + extra) - result.lineTo(self.src_node.pos() - vec - extra) - result.lineTo(self.dest_node.pos() + vec - extra) - result.lineTo(self.dest_node.pos() + vec + extra) - result.closeSubpath() + return self._arc - return result + def paint(self, painter, option, widget): + #self.paint_line(painter, option, widget) + self.paint_arc(painter, option, widget) @timer - def paint(self, painter, option, widget): + def paint_line(self, painter, option, widget): # Draw shape of the edge # color = QColor(Qt.white) # color.setAlpha(128) @@ -191,40 +197,169 @@ class EdgeItem(QGraphicsItem): color, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin ) ) - # Draw the line + painter.drawLine(line) - line_center = QPointF( + # 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 - # Draw the arrows - brush = QBrush() - brush.setColor(color) - brush.setStyle(Qt.SolidPattern) + 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 = line_center + QPointF( + arrow_p1 = center + QPointF( math.sin(angle - math.pi / 3) * size, math.cos(angle - math.pi / 3) * size ) - arrow_p2 = line_center + QPointF( + arrow_p2 = center + QPointF( math.sin(angle - math.pi + math.pi / 3) * size, math.cos(angle - math.pi + math.pi / 3) * size ) - poly = QPolygonF([line_center, arrow_p1, arrow_p2]) + 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__() @@ -321,9 +456,13 @@ class GraphWidget(QGraphicsView): 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) @@ -363,25 +502,40 @@ class GraphWidget(QGraphicsView): curr_edge = self.graph.current_reach() + iedges = [] + multiple_edges = {} for edge in self.graph.edges(): - n1 = list( + n1 = next( filter( lambda n: n.node.name == edge.node1.name, self.node_items ) ) - n2 = list( + n2 = next( filter( lambda n: n.node.name == edge.node2.name, self.node_items ) ) - iedge = EdgeItem(n1[0], n2[0], edge, self) + # 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) diff --git a/src/View/Network/UndoCommand.py b/src/View/Network/UndoCommand.py index 5c752c0de01c1506b9fae7f032020001cd31e118..1a77840ed0c18db653a03c0871a3eba8b65404e4 100644 --- a/src/View/Network/UndoCommand.py +++ b/src/View/Network/UndoCommand.py @@ -40,7 +40,7 @@ class AddNodeCommand(QUndoCommand): self._node = node def undo(self): - self._graph.remove_node(self._node.name) + self._graph.remove_node(self._node) def redo(self): self._graph.insert_node(self._node) @@ -85,9 +85,9 @@ class DelNodeCommand(QUndoCommand): ) for edge in self._edges: - self._graph.remove_edge(edge.name) + self._graph.remove_edge(edge) - self._graph.remove_node(self._node.name) + self._graph.remove_node(self._node) class AddEdgeCommand(QUndoCommand): @@ -98,7 +98,7 @@ class AddEdgeCommand(QUndoCommand): self._edge = edge def undo(self): - self._graph.remove_edge(self._edge.name) + self._graph.remove_edge(self._edge) def redo(self): self._graph.insert_edge(self._edge) @@ -115,7 +115,7 @@ class DelEdgeCommand(QUndoCommand): self._graph.insert_edge(self._edge) def redo(self): - self._graph.remove_edge(self._edge.name) + self._graph.remove_edge(self._edge) class SetCommand(QUndoCommand):