基础的无向图呈现

This commit is contained in:
codeboss 2024-08-02 12:02:24 +08:00
parent b7f71fb29f
commit d4bf6b186c
11 changed files with 645 additions and 109 deletions

View File

@ -5,8 +5,14 @@
</component>
<component name="ChangeListManager">
<list default="true" id="f609c0f2-cd0d-4eea-87f1-8caf02d3f04f" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/graph/undirected_graph/UDGLayout.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/graph/undirected_graph/UDGPresent.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/graph/undirected_graph/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/entry.py" beforeDir="false" afterPath="$PROJECT_DIR$/entry.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frame/ContentView.py" beforeDir="false" afterPath="$PROJECT_DIR$/frame/ContentView.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/graph/DataType.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/DataType.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGPresent.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGPresent.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -34,53 +40,40 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Python.CompareViews.executor&quot;: &quot;Run&quot;,
&quot;Python.CompareWindow.executor&quot;: &quot;Run&quot;,
&quot;Python.ContentView.executor&quot;: &quot;Run&quot;,
&quot;Python.DAGGraph (1).executor&quot;: &quot;Run&quot;,
&quot;Python.DAGGraph.executor&quot;: &quot;Run&quot;,
&quot;Python.DAGLayout (1).executor&quot;: &quot;Run&quot;,
&quot;Python.DAGLayout.executor&quot;: &quot;Run&quot;,
&quot;Python.DAGPresent.executor&quot;: &quot;Run&quot;,
&quot;Python.MergeView.executor&quot;: &quot;Run&quot;,
&quot;Python.MileStone.executor&quot;: &quot;Run&quot;,
&quot;Python.NovelManage.executor&quot;: &quot;Debug&quot;,
&quot;Python.ReferView.executor&quot;: &quot;Run&quot;,
&quot;Python.StoryMap.executor&quot;: &quot;Run&quot;,
&quot;Python.ast_load.executor&quot;: &quot;Debug&quot;,
&quot;Python.entry.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;D:/Projects/Python/StoryCheckTools&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;reference.settings.ide.settings.new.ui&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Python.CompareViews.executor": "Run",
"Python.CompareWindow.executor": "Run",
"Python.ContentView.executor": "Run",
"Python.DAGGraph (1).executor": "Run",
"Python.DAGGraph.executor": "Run",
"Python.DAGLayout (1).executor": "Run",
"Python.DAGLayout.executor": "Run",
"Python.DAGPresent.executor": "Run",
"Python.MergeView.executor": "Run",
"Python.MileStone.executor": "Run",
"Python.NovelManage.executor": "Debug",
"Python.ReferView.executor": "Run",
"Python.StoryMap.executor": "Run",
"Python.UDGLayout.executor": "Run",
"Python.UDGPresent.executor": "Run",
"Python.ast_load.executor": "Debug",
"Python.entry.executor": "Run",
"Python.test.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "D:/Projects/Python/StoryTools",
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"
}
}</component>
<component name="RunManager" selected="Python.ContentView">
<configuration name="CompareViews" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/frame" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/frame/CompareViews.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
}]]></component>
<component name="PyDebuggerOptionsProvider">
<option name="mySaveCallSignatures" value="true" />
<option name="mySupportGeventDebugging" value="true" />
<option name="myDropIntoDebuggerOnFailedTests" value="true" />
<option name="myPyQtBackend" value="pyqt5" />
</component>
<component name="RunManager" selected="Python.UDGPresent">
<configuration name="ContentView" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
@ -103,28 +96,6 @@
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="DAGLayout" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/graph/directed_acyclic_graph" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="DAGPresent" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
@ -147,7 +118,7 @@
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="NovelManage" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<configuration name="UDGLayout" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
@ -156,12 +127,56 @@
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/manage" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/graph/undirected_graph" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/manage/NovelManage.py" />
<option name="PARAMETERS" value="wnss -cmp" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/graph/undirected_graph/UDGLayout.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="UDGPresent" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/graph/undirected_graph" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/graph/undirected_graph/UDGPresent.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/graph/undirected_graph" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/graph/undirected_graph/test.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
@ -170,26 +185,26 @@
<method v="2" />
</configuration>
<list>
<item itemvalue="Python.UDGPresent" />
<item itemvalue="Python.test" />
<item itemvalue="Python.UDGLayout" />
<item itemvalue="Python.ContentView" />
<item itemvalue="Python.DAGLayout" />
<item itemvalue="Python.DAGPresent" />
<item itemvalue="Python.CompareViews" />
<item itemvalue="Python.NovelManage" />
</list>
<recent_temporary>
<list>
<item itemvalue="Python.UDGPresent" />
<item itemvalue="Python.ContentView" />
<item itemvalue="Python.test" />
<item itemvalue="Python.UDGLayout" />
<item itemvalue="Python.DAGPresent" />
<item itemvalue="Python.DAGLayout" />
<item itemvalue="Python.CompareViews" />
<item itemvalue="Python.NovelManage" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-python-sdk-5b207ade9991-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.17890.14" />
<option value="bundled-python-sdk-975db3bf15a3-31b6be0877a2-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.18034.82" />
</set>
</attachedChunks>
</component>

View File

@ -1,9 +1 @@
from parse.StoryMap import XAST_ParseTool, storyline_list2map
from parse.ast_load import global_ast_path
astx = XAST_ParseTool(global_ast_path)
storys = astx.story_list
storys_map = storyline_list2map(storys)
astx.storylines_plait(storys_map)
print(storys_map)
print(__file__)

View File

@ -2,7 +2,7 @@ import sys
from typing import Dict, List
from PyQt5.QtCore import QPoint
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout
from PyQt5.QtWidgets import QMenu
from graph.DataType import Arrow, Point
@ -17,13 +17,15 @@ class FragmentPoint(Point):
pass
class ContentWindow(QMainWindow):
class StorylinesView(QWidget):
def __init__(self, parent):
QMainWindow.__init__(self, parent)
QWidget.__init__(self, parent)
layout = QVBoxLayout(self)
self.fragment_view = DAGActiveView(self)
self.setCentralWidget(self.fragment_view)
self.fragment_view.nodes_clicked.connect(self.print_node_list)
self.present_graph: Dict[str, StoryMap] = None
layout.setContentsMargins(0,0,0,0)
layout.addWidget(self.fragment_view)
self.fragment_view.nodes_clicked.connect(self.highlisth_node_path)
self.present_graph: Dict[str, StoryMap] = {}
pass
pass
@ -56,7 +58,7 @@ class ContentWindow(QMainWindow):
self.fragment_view.update_with_edges(arrows)
pass
def print_node_list(self, xpos, ypos, list):
def highlisth_node_path(self, xpos, ypos, list):
if len(list) == 0:
return
@ -90,12 +92,12 @@ class ContentWindow(QMainWindow):
pass
if len(story_list) == 1:
self.print_node_list(xpos, ypos, [("node", story_list[0])])
self.highlisth_node_path(xpos, ypos, [("node", story_list[0])])
elif len(story_list) > 1:
menu = QMenu()
for story in story_list:
def trigger(story_name:str):
return lambda : self.print_node_list(xpos, ypos, [("node", story_name)])
return lambda : self.highlisth_node_path(xpos, ypos, [("node", story_name)])
menu.addAction(f"story/{story}", trigger(story))
pass
@ -111,9 +113,15 @@ class ContentWindow(QMainWindow):
pass
class ArticleRefsView(QWidget):
def __init__(self, parent):
QWidget.__init__(self, parent)
pass
if __name__ == "__main__":
app = QApplication(sys.argv)
view = ContentWindow(None)
view = StorylinesView(None)
view.show()
tool = XAST_ParseTool(global_ast_path)

View File

@ -1,3 +1,4 @@
import math
from typing import List
@ -7,6 +8,44 @@ class Pos:
self.y_pos = y
pass
def __add__(self, other: 'Pos') -> 'Pos':
return Pos(self.x_pos + other.x_pos, self.y_pos + other.y_pos)
def __sub__(self, other: 'Pos') -> 'Pos':
return Pos(self.x_pos - other.x_pos, self.y_pos - other.y_pos)
def __iadd__(self, other: 'Pos') -> 'Pos':
self.x_pos += other.x_pos
self.y_pos += other.y_pos
return self
def __isub__(self, other: 'Pos') -> 'Pos':
self.x_pos -= other.x_pos
self.y_pos -= other.y_pos
return self
def __mul__(self, t: float) -> 'Pos':
return Pos(self.x_pos * t, self.y_pos * t)
def __imul__(self, t: float) -> 'Pos':
self.x_pos *= t
self.y_pos *= t
return self
def vec_length(self) -> float:
return math.sqrt(self.x_pos**2 + self.y_pos**2)
def normalized(self) -> 'Pos':
dist = self.vec_length()
if dist == 0:
return Pos(0, 0)
return self * (1/dist)
def to_text(self) -> str:
return f"pos<{self.__hash__()}>{{{self.x_pos}, {self.y_pos}}}"
def make_copy(self) -> 'Pos':
return Pos(self.x_pos, self.y_pos)
@ -23,6 +62,16 @@ class Point:
return Point(self.point_name)
class PositionalPoint(Point, Pos):
def __init__(self, name: str, x: float, y: float):
Point.__init__(self, name)
Pos.__init__(self, x, y)
pass
def make_copy(self) -> 'PositionalPoint':
return PositionalPoint(self.name(), self.x_pos, self.y_pos)
class Line:
def __init__(self, p0: Point, p1: Point):
self.point_set = [p0, p1]
@ -46,5 +95,3 @@ class Arrow(Line):
def end_point(self):
return self.point_set[1]
pass

View File

@ -422,18 +422,19 @@ class DAGActiveView(QGraphicsView):
def mousePressEvent(self, event: QMouseEvent):
QGraphicsView.mousePressEvent(self, event)
gitems = self.items(event.pos())
noderef_names = []
for gnode in gitems:
if gnode.node_key_bind[0].startswith("node"):
noderef_names.append(gnode.node_key_bind)
if event.button() == Qt.MouseButton.LeftButton:
gitems = self.items(event.pos())
noderef_names = []
for gnode in gitems:
if gnode.node_key_bind[0].startswith("node"):
noderef_names.append(gnode.node_key_bind)
pass
pass
pass
self.nodes_clicked.emit(event.pos().x(), event.pos().y(), noderef_names[0:1])
self.nodes_clicked.emit(event.pos().x(), event.pos().y(), noderef_names[0:1])
pass
if __name__ == "__main__":
app = QApplication(sys.argv)
view = DAGActiveView(None)

View File

@ -0,0 +1,167 @@
import sys, os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
from graph.DataType import PositionalPoint, Line, Pos, Point
from typing import List, Dict
import random as rd
class ForceCalcHelper:
"""
力导向计算辅助节点
"""
def __init__(self, init_v: PositionalPoint):
self.bind_point = init_v
self.sibling_nodes: Dict[str, 'ForceCalcHelper'] = {}
pass
def bind_point_name(self) -> str:
return self.bind_point.point_name
def get_sibling_nodes(self) -> Dict[str, 'ForceCalcHelper']:
return self.sibling_nodes
pass
def sibling_append(self, node: 'ForceCalcHelper'):
self.sibling_nodes[node.bind_point_name()] = node
pass
class UDGGraph:
force2accx_rates = 1
def __init__(self):
self.random_gen = rd.Random()
self.node_set: Dict[str, ForceCalcHelper] = {}
pass
def rebuild_from_edges(self, line_set: List[Line]):
self.node_set.clear()
for line in line_set:
start_node = line.points()[0]
if start_node.point_name not in self.node_set:
pos_node = PositionalPoint(start_node.point_name, 0, 0)
self.node_set[start_node.point_name] = ForceCalcHelper(pos_node)
pass
end_node = line.points()[1]
if start_node.point_name == end_node.point_name:
continue
if end_node.point_name not in self.node_set:
pos_node = PositionalPoint(end_node.point_name, 0, 0)
self.node_set[end_node.point_name] = ForceCalcHelper(pos_node)
pass
start_force_point = self.node_set[start_node.point_name]
other_force_point = self.node_set[end_node.point_name]
start_force_point.sibling_append(other_force_point)
other_force_point.sibling_append(start_force_point)
pass
pass
def __eject_with_item(self, curr: ForceCalcHelper, node_set:Dict[str, ForceCalcHelper]) -> Pos:
init_value = Pos(0, 0)
for node in node_set.values():
if curr.bind_point_name() == node.bind_point_name():
continue
coord_span = curr.current_pos() - node.current_pos()
distance = coord_span.vec_length()
force_scalar = ForceCalcHelper.eject_k / (distance**2)
force_vec = coord_span.normalized() * force_scalar
init_value += force_vec
pass
return init_value
def __attract_with_item_sibs(self, curr: ForceCalcHelper) -> Pos:
init_value = Pos(0, 0)
for node in curr.get_sibling_nodes().values():
coord_span = curr.current_pos() - node.current_pos()
distance = coord_span.vec_length()
force_scalar = distance * ForceCalcHelper.attract_k
force_vec = coord_span.normalized() * force_scalar
init_value -= force_vec
pass
return init_value
def __calculate_item_force(self, curr: ForceCalcHelper, node_set:Dict[str, ForceCalcHelper]) -> float:
"""
计算指定节点对整个数据图节点的合力
:param times: 次数初始迭代有特殊处理
:param curr: 当前节点
:param node_set: 所有节点
:return: 合力标量
"""
eject_vec2 = self.__eject_with_item(curr, node_set)
attract_vec2 = self.__attract_with_item_sibs(curr)
curr.force_with_direction = eject_vec2 + attract_vec2
# 阻尼计算 f=fxG(fxg=1,m=1)
fr = ForceCalcHelper.damping_k * 10
if curr.force_with_direction.vec_length() > fr:
curr.force_with_direction -= curr.force_with_direction.normalized() * fr
pass
else:
curr.force_with_direction = Pos()
pass
return curr.force_with_direction.vec_length()
def __item_position_adjust(self, curr: ForceCalcHelper) -> None:
if curr.force_with_direction.vec_length() == 0:
return
vec_speed = curr.force_with_direction.normalized()
curr.move_by(vec_speed)
pass
def graph_layout(self):
for curr in self.node_set.values():
random_pos = Pos(self.random_gen.random() * 100, self.random_gen.random() * 100)
curr.move_by(random_pos)
pass
for idx in range(0, 10):
for curr in self.node_set.values():
self.__calculate_item_force(curr, self.node_set)
pass
for curr in self.node_set.values():
self.__item_position_adjust(curr)
pass
pass
pass
def visible_positon_set(self) -> List[PositionalPoint]:
retvs = []
for node in self.node_set.values():
retvs.append(node.bind_point)
pass
return retvs
if __name__ == "__main__":
list_in = [
Line(Point("a"), Point("b")),
Line(Point("a"), Point("c")),
Line(Point("a"), Point("d")),
Line(Point("a"), Point("e")),
Line(Point("d"), Point("e")),
Line(Point("f"), Point("c")),
]
graph = UDGGraph()
graph.rebuild_from_edges(list_in)
graph.graph_layout()
for p in graph.visible_positon_set():
print(f"node:{p.name()}<{p.x_pos},{p.y_pos}>")
pass
pass

View File

@ -0,0 +1,285 @@
import sys
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 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")
class PresentNode(QGraphicsItem, GraphNode):
def __init__(self, name: str, font: QFont, parent):
QGraphicsItem.__init__(self, parent)
self.node_name = name
self.__is_highlight_mark = False
self.__font_bind = font
self.__sibling_list = []
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
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):
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)
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.__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()
edge_set: Dict[str, Tuple[GraphNode, GraphNode]] = {}
for line in line_set:
start_node = line.points()[0]
if start_node.point_name not in self.node_set:
self.node_set[start_node.point_name] = PresentNode(start_node.point_name, self.font(), None)
pass
self.__layout_graph.add_node(start_node.point_name)
end_node = line.points()[1]
if start_node.point_name == end_node.point_name:
continue
if end_node.point_name not in self.node_set:
self.node_set[end_node.point_name] = PresentNode(end_node.point_name, self.font(), None)
pass
self.__layout_graph.add_node(end_node.point_name)
self.__layout_graph.add_edge(start_node.point_name, end_node.point_name)
start_force_point: PresentNode = self.node_set[start_node.point_name]
other_force_point: PresentNode = self.node_set[end_node.point_name]
if other_force_point not in start_force_point.sibling_nodes():
start_force_point.sibling_append(other_force_point)
if start_force_point not in other_force_point.sibling_nodes():
other_force_point.sibling_append(start_force_point)
pass
pos_map = nx.spring_layout(self.__layout_graph)
scala_value:float = 0
for name in pos_map:
primitive_pos = pos_map[name]
target_gnode: PresentNode = self.node_set[name]
sibling_nodes = target_gnode.sibling_nodes()
for sib in sibling_nodes:
sib_primitive_pos = pos_map[sib.node_name]
prim_x_span = primitive_pos[0] - sib_primitive_pos[0]
prim_y_span = primitive_pos[1] - sib_primitive_pos[1]
target_rect = target_gnode.boundingRect()
scala_value = max(scala_value, target_rect.width()/prim_x_span)
scala_value = max(scala_value, target_rect.height()/prim_y_span)
pass
pass
for name in pos_map:
primitive_pos = pos_map[name]
target_gnode: PresentNode = self.node_set[name]
target_gnode.setPos(primitive_pos[0] * scala_value, primitive_pos[1] * scala_value)
self.__scene_bind.addItem(target_gnode)
pass
for edge in nx.edges(self.__layout_graph):
edge_start = edge[0]
edge_end = edge[1]
node_one = self.node_set[edge_start]
node_two = self.node_set[edge_end]
connection = ConnectionNode(node_one, node_two, None)
self.scene().addItem(connection)
connection.relayout_exec()
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.node_type() == PresentNodeType.PresentNode:
vnode: PresentNode = item
self.node_clicked.emit(vnode.node_name)
print(vnode.node_name)
pass
pass
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()

View File

View File

@ -0,0 +1,21 @@
import sys, os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
import networkx as nx
import matplotlib.pyplot as plt
g = nx.Graph()
g.add_node('a')
g.add_node('b')
g.add_node('c')
g.add_node('d')
g.add_edge('a', 'b')
g.add_edge('b','c')
g.add_edge('b','d')
posx = nx.spring_layout(g)
print(posx)
for n in posx:
print(n)
nx.draw(g, posx)
plt.show()