338 lines
10 KiB
Python
338 lines
10 KiB
Python
import sys, os
|
|
from abc import abstractmethod
|
|
from enum import Enum
|
|
from typing import List, Dict, Tuple
|
|
|
|
import networkx as nx
|
|
from PyQt5.QtCore import QPointF, QRectF, Qt, pyqtSignal
|
|
from PyQt5.QtGui import QFont, QPainterPath, QPen, QPainter, QMouseEvent
|
|
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsView, QApplication, QGraphicsScene
|
|
from PyQt5.QtWidgets import QSplitter, QHBoxLayout
|
|
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
from graph.DataType import Point, Line
|
|
|
|
|
|
class PresentNodeType(Enum):
|
|
PresentNode = 0,
|
|
ConnectionNode = 1,
|
|
|
|
|
|
class GraphNode:
|
|
@abstractmethod
|
|
def node_type(self) -> PresentNodeType:
|
|
raise NotImplementedError("node_type")
|
|
|
|
@abstractmethod
|
|
def highlight(self, flag: bool):
|
|
raise NotImplementedError("highlight")
|
|
|
|
@abstractmethod
|
|
def is_highlighted(self) -> bool:
|
|
raise NotImplementedError("is_highlighted")
|
|
|
|
@abstractmethod
|
|
def boundingRect(self) -> QRectF:
|
|
raise NotImplementedError("boundingRect")
|
|
|
|
@abstractmethod
|
|
def pos(self) -> QPointF:
|
|
raise NotImplementedError("pos")
|
|
|
|
@abstractmethod
|
|
def relayout_exec(self, ppu: float = 1):
|
|
raise NotImplementedError("relayout_exec")
|
|
|
|
|
|
class PresentNode(QGraphicsItem, GraphNode):
|
|
def __init__(self, name: str, font: QFont, prim_x:float, prim_y:float, parent):
|
|
QGraphicsItem.__init__(self, parent)
|
|
self.node_name = name
|
|
self.__is_highlight_mark = False
|
|
self.__font_bind = font
|
|
self.__sibling_list = []
|
|
self.__prim_pos: Tuple[float, float] = (prim_x, prim_y)
|
|
self.setZValue(10)
|
|
pass
|
|
|
|
def sibling_append(self, node: 'PresentNode'):
|
|
if node not in self.__sibling_list:
|
|
self.__sibling_list.append(node)
|
|
pass
|
|
pass
|
|
|
|
def sibling_nodes(self) -> List['PresentNode']:
|
|
return self.__sibling_list
|
|
|
|
def node_type(self) -> PresentNodeType:
|
|
return PresentNodeType.PresentNode
|
|
|
|
def highlight(self, flag: bool):
|
|
self.__is_highlight_mark = flag
|
|
pass
|
|
|
|
def is_highlighted(self) -> bool:
|
|
return self.__is_highlight_mark
|
|
pass
|
|
|
|
def boundingRect(self) -> QRectF:
|
|
width_x = self.__font_bind.pixelSize() * (len(self.node_name)+1)
|
|
height_x = self.__font_bind.pixelSize()
|
|
return QRectF(0, 0, width_x + 10, height_x + 10)
|
|
pass
|
|
|
|
def paint(self, painter, option, widget = ...):
|
|
outline = self.boundingRect()
|
|
|
|
path_icon = QPainterPath()
|
|
path_icon.lineTo(outline.height()/2 - 5, outline.height() -5)
|
|
path_icon.lineTo(outline.height()/2, outline.height()/2)
|
|
path_icon.lineTo(outline.height() - 5, outline.height()/2 - 5)
|
|
path_icon.lineTo(0, 0)
|
|
|
|
painter.save()
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
#painter.drawRect(outline)
|
|
|
|
if self.__is_highlight_mark:
|
|
brush = Qt.red
|
|
painter.setPen(Qt.red)
|
|
else:
|
|
brush = Qt.black
|
|
painter.setPen(Qt.black)
|
|
pass
|
|
|
|
painter.fillPath(path_icon, brush)
|
|
painter.translate(outline.height(), 5)
|
|
painter.drawText(outline, self.node_name)
|
|
|
|
painter.restore()
|
|
pass
|
|
|
|
def relayout_exec(self, ppu: float = 1):
|
|
pos_x = ppu * self.__prim_pos[0]
|
|
pos_y = ppu * self.__prim_pos[1]
|
|
self.setPos(pos_x, pos_y)
|
|
self.update()
|
|
pass
|
|
|
|
|
|
class ConnectionNode(QGraphicsItem, GraphNode):
|
|
def __init__(self, p0: GraphNode, p1: GraphNode, parent):
|
|
QGraphicsItem.__init__(self, parent)
|
|
self.__highlight_mark = False
|
|
self.__point0 = p0
|
|
self.__point1 = p1
|
|
self.__outline = QRectF()
|
|
self.setZValue(1)
|
|
pass
|
|
|
|
def node_type(self) -> PresentNodeType:
|
|
return PresentNodeType.ConnectionNode
|
|
|
|
def highlight(self, flag: bool):
|
|
self.__highlight_mark = flag
|
|
pass
|
|
|
|
def is_highlighted(self) -> bool:
|
|
return self.__highlight_mark
|
|
pass
|
|
|
|
def relayout_exec(self, ppi: float = 1):
|
|
start_pos = self.__point0.pos()
|
|
end_pos = self.__point1.pos()
|
|
|
|
start_x = min(start_pos.x(), end_pos.x())
|
|
start_y = min(start_pos.y(), end_pos.y())
|
|
end_x = max(start_pos.x(), end_pos.x())
|
|
end_y = max(start_pos.y(), end_pos.y())
|
|
|
|
self.setPos(QPointF(start_x, start_y))
|
|
self.__outline = QRectF(0, 0, end_x - start_x, end_y - start_y)
|
|
self.update()
|
|
pass
|
|
|
|
def boundingRect(self):
|
|
return self.__outline
|
|
pass
|
|
|
|
def paint(self, painter, option, widget = ...):
|
|
start_pos = self.__point0.pos()
|
|
end_pos = self.__point1.pos()
|
|
outline = self.boundingRect()
|
|
|
|
painter.save()
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
if self.__highlight_mark:
|
|
pen = QPen(Qt.red)
|
|
else:
|
|
pen = QPen(Qt.lightGray)
|
|
|
|
pen.setWidthF(3)
|
|
painter.setPen(pen)
|
|
if start_pos.y() < end_pos.y():
|
|
if start_pos.x() < end_pos.x():
|
|
painter.drawLine(outline.topLeft(), outline.bottomRight())
|
|
else:
|
|
painter.drawLine(outline.topRight(), outline.bottomLeft())
|
|
else:
|
|
if start_pos.x() < end_pos.x():
|
|
painter.drawLine(outline.bottomLeft(), outline.topRight())
|
|
else:
|
|
painter.drawLine(outline.topLeft(), outline.bottomRight())
|
|
|
|
painter.restore()
|
|
pass
|
|
|
|
|
|
class UDGPresent(QGraphicsView):
|
|
node_clicked = pyqtSignal(str)
|
|
|
|
def __init__(self, parent):
|
|
QGraphicsView.__init__(self, parent)
|
|
self.pixel_per_unit = 5000
|
|
|
|
self.__highlight_nodes: List[GraphNode] = []
|
|
self.node_set: Dict[str, GraphNode] = {}
|
|
self.__layout_graph = nx.Graph()
|
|
self.__scene_bind = QGraphicsScene(self)
|
|
self.setScene(self.__scene_bind)
|
|
|
|
font = QFont()
|
|
font.setPixelSize(20)
|
|
self.setFont(font)
|
|
pass
|
|
|
|
def rebuild_from_edges(self, line_set: List[Line]):
|
|
self.node_set.clear()
|
|
|
|
for line in line_set:
|
|
start_node = line.points()[0]
|
|
self.__layout_graph.add_node(start_node.point_name)
|
|
|
|
end_node = line.points()[1]
|
|
self.__layout_graph.add_node(end_node.point_name)
|
|
|
|
self.__layout_graph.add_edge(start_node.point_name, end_node.point_name)
|
|
pass
|
|
|
|
pos_map = nx.spring_layout(self.__layout_graph)
|
|
for node_name in pos_map:
|
|
node_prim_pos = pos_map[node_name]
|
|
targetx_node = PresentNode(node_name, self.font(), node_prim_pos[0], node_prim_pos[1], None)
|
|
self.node_set[node_name] = targetx_node
|
|
|
|
self.scene().addItem(targetx_node)
|
|
targetx_node.relayout_exec(self.pixel_per_unit)
|
|
pass
|
|
|
|
for edge in nx.edges(self.__layout_graph):
|
|
edge_start = edge[0]
|
|
edge_end = edge[1]
|
|
node_one: PresentNode = self.node_set[edge_start]
|
|
node_two: PresentNode = self.node_set[edge_end]
|
|
node_one.sibling_append(node_two)
|
|
node_two.sibling_append(node_one)
|
|
|
|
connection = ConnectionNode(node_one, node_two, None)
|
|
self.scene().addItem(connection)
|
|
connection.relayout_exec(self.pixel_per_unit)
|
|
self.node_set[f"conn::{edge_start}&{edge_end}"] = connection
|
|
pass
|
|
pass
|
|
|
|
def mousePressEvent(self, event: QMouseEvent):
|
|
QGraphicsView.mousePressEvent(self, event)
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
item: GraphNode = self.itemAt(event.pos())
|
|
if item is not None and item.node_type() == PresentNodeType.PresentNode:
|
|
vnode: PresentNode = item
|
|
self.node_clicked.emit(vnode.node_name)
|
|
pass
|
|
pass
|
|
pass
|
|
|
|
def refresh_with_ppu(self, ppu: float):
|
|
self.pixel_per_unit = ppu
|
|
|
|
for node in self.node_set.values():
|
|
if node.node_type() == PresentNodeType.PresentNode:
|
|
node.relayout_exec(ppu)
|
|
pass
|
|
pass
|
|
|
|
for node in self.node_set.values():
|
|
if node.node_type() == PresentNodeType.ConnectionNode:
|
|
node.relayout_exec(ppu)
|
|
pass
|
|
pass
|
|
|
|
self.scene().update()
|
|
self.update()
|
|
pass
|
|
|
|
def update_scene_rect(self):
|
|
minx = 2*64
|
|
miny = 2*64
|
|
maxx = -2**64
|
|
maxy = -2**64
|
|
for item in self.node_set.values():
|
|
minx = min(item.pos().x(), minx)
|
|
miny = min(item.pos().y(), miny)
|
|
maxx = max(item.pos().x() + item.boundingRect().width(), maxx)
|
|
maxy = max(item.pos().y() + item.boundingRect().height(), maxy)
|
|
pass
|
|
|
|
self.scene().setSceneRect(minx, miny, maxx - minx, maxy - miny)
|
|
pass
|
|
|
|
def highlight_sibling_nodes(self, center_node: str):
|
|
for node in self.__highlight_nodes:
|
|
node.highlight(False)
|
|
pass
|
|
self.__highlight_nodes.clear()
|
|
|
|
target_node: PresentNode = self.node_set[center_node]
|
|
if target_node is None:
|
|
return
|
|
|
|
target_node.highlight(True)
|
|
self.__highlight_nodes.append(target_node)
|
|
for sib_node in target_node.sibling_nodes():
|
|
sib_node.highlight(True)
|
|
self.__highlight_nodes.append(sib_node)
|
|
|
|
sib_name = sib_node.node_name
|
|
constr1 = f"conn::{center_node}&{sib_name}"
|
|
constr2 = f"conn::{sib_name}&{center_node}"
|
|
if constr1 in self.node_set:
|
|
self.node_set[constr1].highlight(True)
|
|
self.__highlight_nodes.append(self.node_set[constr1])
|
|
elif constr2 in self.node_set:
|
|
self.node_set[constr2].highlight(True)
|
|
self.__highlight_nodes.append(self.node_set[constr2])
|
|
pass
|
|
pass
|
|
|
|
self.scene().update()
|
|
self.update()
|
|
pass
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = QApplication(sys.argv)
|
|
view = UDGPresent(None)
|
|
view.show()
|
|
|
|
list_in = [
|
|
Line(Point('a中古'), Point('b')),
|
|
Line(Point('a中古'), Point('c')),
|
|
Line(Point('a中古'), Point('d')),
|
|
]
|
|
|
|
view.rebuild_from_edges(list_in)
|
|
|
|
app.exec() |