diff --git a/python/graph_collapse_helpers.py b/python/graph_collapse_helpers.py index 4cb2396c6926c208c04b8231b959b25738d2bfa3..c28da4402b7284a0948f78bb2db3ba65a7a36b1d 100644 --- a/python/graph_collapse_helpers.py +++ b/python/graph_collapse_helpers.py @@ -91,17 +91,17 @@ def at_and_above_in_uncollapsed(node): return node_set # get the set of removable_edges and addable_edge_pairs -def collapsed_edges(graph_item): +def collapsed_edges(graph_scene): print("collapsed_edges.a") # make sure there are edges to work with - if not graph_item.edges: + if not graph_scene.edges: return (list(), list()) # identify the existing collapsed source_node, dest_node edge pairs existing_edge_pairs = set() existing_edges = dict() - for edge in graph_item.edges: + for edge in graph_scene.edges: if edge.relation == "COLLAPSED_FOLLOWS": existing_edge_pairs.add((edge.source_node, edge.dest_node)) existing_edges[(edge.source_node, edge.dest_node)] = edge @@ -110,7 +110,7 @@ def collapsed_edges(graph_item): # calculate the set of source_node, dest_node edge pairs edge_pairs = set() - for edge in graph_item.edges: + for edge in graph_scene.edges: if edge.relation == "FOLLOWS" and (edge.source_node.collapse or edge.dest_node.collapse): print("will add COLLAPSED_FOLLOWS to bridge: %s to %s" % ( diff --git a/python/graph_constants.py b/python/graph_constants.py deleted file mode 100644 index 72c44fdb3a307b1ff63cdcdc5ef8247745f050af..0000000000000000000000000000000000000000 --- a/python/graph_constants.py +++ /dev/null @@ -1,5 +0,0 @@ -from PyQt5.QtCore import QRectF - -# global constants and defaults -GRAPHICS_RECT = QRectF(-50, -50, 25000, 25000) - diff --git a/python/graph_item.py b/python/graph_item.py index b7ee8eab1faa1defd19ff12fb4910d59af972ba7..839efd0fcf1ebc8ff887af07e5f70bec4d5a2180 100644 --- a/python/graph_item.py +++ b/python/graph_item.py @@ -1,5 +1,4 @@ from PyQt5.QtCore import QRect -from graph_constants import GRAPHICS_RECT from graph_collapse_helpers import collapsed_edges from edge import Edge from edge_point_placer import EdgePointPlacer @@ -12,7 +11,7 @@ class GraphItem(): * nodes (list<Node>) * edges (list<Edge>) - NOTE: Optimization: call initialize_items and appearance just-in-time. + NOTE: Optimization: call initialize_items and set_appearance just-in-time. Set appearance when graph should be painted differently. """ @@ -64,23 +63,7 @@ class GraphItem(): # perform any just-in-time initialization self.initialize_items() self.set_appearance() - - # find the corners of this graph - min_x = GRAPHICS_RECT.x() + GRAPHICS_RECT.width() - max_x = GRAPHICS_RECT.x() - min_y = GRAPHICS_RECT.y() + GRAPHICS_RECT.height() - max_y = GRAPHICS_RECT.y() - for node in self.nodes: - if node.x() - node.w/2 < min_x: - min_x = node.x() - node.w/2 - if node.x() + node.w/2 > max_x: - max_x = node.x() + node.w/2 - if node.y() - node.h/2 < min_y: - min_y = node.y() - node.h/2 - if node.y() + node.h/2 > max_y: - max_y = node.y() + node.h/2 - - return QRect(min_x, min_y, max_x-min_x, max_y-min_y) + return self.itemsBoundingRect() # uncollapse all nodes of node type def uncollapse(self, node_type): diff --git a/python/graph_main_widget.py b/python/graph_main_widget.py index 3297bda567b76f9edffe8de4fdd06212bd44d21b..0a9160a4ef16d13831264d3a800bb9d9457cb3d8 100644 --- a/python/graph_main_widget.py +++ b/python/graph_main_widget.py @@ -26,14 +26,12 @@ from edge_grip import EdgeGrip # GraphicsView class GraphMainView(QGraphicsView): - """GraphMainWidget provides the main QGraphicsView. It manages signals - and wraps these: - * GraphMainView - * GraphMainScene + """GraphMainWidget provides the main QGraphicsView. + It manages signals. GraphMainWidget also issues signal: - * signal_graph_item_view_changed = pyqtSignal(GraphItem, - name='graphItemViewChanged') + * signal_graph_item_view_changed = pyqtSignal(dict, + name='graphViewChanged') """ def __init__(self): diff --git a/python/graph_scene.py b/python/graph_scene.py new file mode 100644 index 0000000000000000000000000000000000000000..70743b4d3968fda50965f37e394d5640dca44db0 --- /dev/null +++ b/python/graph_scene.py @@ -0,0 +1,214 @@ +import math +from PyQt5.QtCore import QRectF, Qt +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import QObject +from PyQt5.QtGui import QBrush, QColor, QLinearGradient, QPainter, QPen +from PyQt5.QtGui import QTransform +from PyQt5.QtWidgets import QGraphicsView +from PyQt5.QtWidgets import QGraphicsScene +from settings_manager import settings +from graph_collapse_helpers import collapse_below, uncollapse_below +#from node import Node # for Node detection +from edge_grip import EdgeGrip + +# QGraphicsScene for a trace graph +class GraphScene(QGraphicsScene): + + def __init__(self, trace): + super(GraphScene, self).__init__() + self.index = trace["index"] + self.mark = trace["mark"] + self.probability = trace["probability"] + self.nodes = trace["nodes"] + self.edges = trace["edges"] + self.graph_views = trace["trace_views"] # zz future + + # edge highlight state, grips show when edges are highlighted + self._highlighted_edge = None + self._source_edge_grip = None + self._dest_edge_grip = None + + self.initialize_items() + self.set_appearance() + + # set_scene, useful in hide/collapse nodes + def set_scene(self): + self.clear_scene() + + # optimization: just-in-time for display + self.initialize_items() + self.set_appearance() + + # add node items + for node in self.nodes: + self.addItem(node) + + # add edge items + for edge in self.edges: + self.addItem(edge) + + # set initial size + self.setSceneRect(self.itemsBoundingRect()) + + # clear_scene, useful in hide/collapse nodes + def clear_scene(self): + # turn off grip selection and grips + if self._highlighted_edge: + self.unhighlight_edge() + + # we must call removeItem so QGraphicsScene does not delete these + for item in self.items(): + item.prepareGeometryChange() + self.removeItem(item) + +# # hack to remove cached residue from edges +# self.clear() + + # perform orderly just-in-time initialization of graph item components + def initialize_items(self): + if not self._items_initialized: + # initialize items + for node in self.nodes: + node.initialize_node(self) + placer = EdgePointPlacer() + for edge in self.edges: + edge.initialize_edge(placer) + self._items_initialized = True + + # reset appearance when graph color or shape changes + def set_appearance(self): + if not self.appearance_set: + for node in self.nodes: + node.set_appearance() + for edge in self.edges: + edge.set_appearance() + self.appearance_set = True + + def change_h_spacing(self, old_spacing, old_indent, + new_spacing, new_indent): + for node in self.nodes: + x = (node.x() - old_indent) / old_spacing * new_spacing + new_indent + node.setPos(x, node.y()) + + def change_v_spacing(self, old_spacing, old_indent, + new_spacing, new_indent): + for node in self.nodes: + y = (node.y() - old_indent) / old_spacing * new_spacing + new_indent + node.setPos(node.x(), y) + + def bounding_rect(self): + # perform any just-in-time initialization + self.set_appearance() + return self.itemsBoundingRect() + + # highlight edge and create its two bezier edge grips + def highlight_edge(self, edge): + if self._highlighted_edge: + raise Exceiption("Bad") + edge.set_highlighted(True) + self._source_edge_grip = EdgeGrip(edge, "source") + self.addItem(self._source_edge_grip) + self._dest_edge_grip = EdgeGrip(edge, "dest") + self.addItem(self._dest_edge_grip) + self._highlighted_edge = edge + + # unhighlight edge and remove its two bezier edge grips + def unhighlight_edge(self): + if not self._highlighted_edge: + raise Exceiption("Bad") + self._highlighted_edge.set_highlighted(False) + # be careful with scene internal indexes else Qt crashes when deleting + self._source_edge_grip.prepareGeometryChange() + self.removeItem(self._source_edge_grip) + self._source_edge_grip = None + self._dest_edge_grip.prepareGeometryChange() + self.removeItem(self._dest_edge_grip) + self._dest_edge_grip = None + self._highlighted_edge = None + + # uncollapse all nodes of node type + def uncollapse(self, node_type): + for node in self.nodes: + if node.node_type == node_type: + node.do_uncollapse() + self.appearance_set = False + self.set_appearance() + self.set_collapsed_edges() + + # diagnostics + def _print_totals(self): + if not self.edges: + print("Totals: None, empty graph.") + return + + # total items in scene + scene_item_total = len(self.items()) + + # nodes + edges in this scene + graph_item_total = len(self.nodes) + len(self.edges) + + # edge_list_total + edge_list_total = 0 + for node in self.nodes: + edge_list_total += len(node.edge_list) + + print("Totals: scene: %d, graph: %d, graph nodes: %d, " + "graph edges: %d, edge list total: %d" % ( + scene_item_total, graph_item_total, + len(self.nodes), len(self.edges), edge_list_total)) + + # redo the list of collapsed edges in edges[] based on what is collapsed + def set_collapsed_edges(self): + print("set_collapsed_edges.a") + self._print_totals() + removable_edges, addable_edge_pairs = collapsed_edges(self) + + # a reference to node_lookup + node_lookup = self.edges[0].node_lookup + + # create the addable edges + addable_edges = list() + for source_node, dest_node in addable_edge_pairs: + addable_edge = Edge(source_node.node_id, "COLLAPSED_FOLLOWS", + dest_node.node_id, "", node_lookup) + addable_edges.append(addable_edge) + + # remove any discontinued FOLLOWS edges + for removable_edge in removable_edges: + removable_edge.source_node.edge_list.remove(removable_edge) + removable_edge.dest_node.edge_list.remove(removable_edge) + self.edges.remove(removable_edge) + + # add any new FOLLOWS edges + placer = EdgePointPlacer() + for addable_edge in addable_edges: + self.edges.append(addable_edge) + addable_edge.initialize_edge(placer) + addable_edge.set_appearance() + + # change the COLLAPSED_FOLLOWS edges + self.clear_scene() + self.set_scene(self) + + # reset appearance + self.appearance_set = False + self.set_appearance() + + print("set_collapsed_edges.b") + self._print_totals() + + # remove grips on mouse down unless on grip + def mousePressEvent(self, event): + if self._highlighted_edge: + if not self._source_edge_grip.isUnderMouse() \ + and not self._dest_edge_grip.isUnderMouse(): + self.unhighlight_edge() + + super(GraphScene, self).mousePressEvent(event) + + # allow QGraphicsScene size to shrink back + def mouseMoveEvent(self, _event): + self.setSceneRect(self.itemsBoundingRect()) + super(GraphScene, self).mouseMoveEvent(_event) + diff --git a/python/graphs_manager.py b/python/graphs_manager.py index 63ca2eb6cbed16212f190b47fb835e366cb4e8da..8e6aa0637c43edefa28cb30a4d4f500786394330 100644 --- a/python/graphs_manager.py +++ b/python/graphs_manager.py @@ -3,15 +3,15 @@ from PyQt5.QtCore import pyqtSignal # for signal/slot support from PyQt5.QtCore import pyqtSlot # for signal/slot support class GraphsManager(QObject): - """Provides graphs (list<GraphItem>) and signals when the graph list + """Provides graphs (dict<dict>) and signals when the graph dict is loaded or cleared. - Register with signal_graphs_loaded to provide current graph list. + Register with signal_graphs_loaded to provide current graph dict. Data structures: * schema_name (str) * scope (int) - * graphs (list<GraphItem>) + * graphs (dict<dict>) Signals: * signal_graphs_loaded() - graphs were loaded or cleared @@ -48,17 +48,18 @@ class GraphsManager(QObject): def appearance_changed(self, old_settings, new_settings): # make all graphs need appearance_set for graph in self.graphs: - graph.appearance_set = False + if "graph_item" in graph: + graph.appearance_set = False - # maybe change spacing - if old_settings["graph_h_spacing"] != new_settings["graph_h_spacing"]: - for graph in self.graphs: + # maybe change spacing + if old_settings["graph_h_spacing"] \ + != new_settings["graph_h_spacing"]: graph.change_h_spacing(old_settings["graph_h_spacing"], old_settings["node_width"]/2, new_settings["graph_h_spacing"], new_settings["node_width"]/2) - if old_settings["graph_v_spacing"] != new_settings["graph_v_spacing"]: - for graph in self.graphs: + if old_settings["graph_v_spacing"] + != new_settings["graph_v_spacing"]: graph.change_v_spacing(old_settings["graph_v_spacing"], old_settings["node_height"]/2, new_settings["graph_v_spacing"], @@ -69,10 +70,8 @@ class GraphsManager(QObject): # return graph associated with graph_index else None def find_graph(self, graph_index): - for graph in self.graphs: - if graph.index == graph_index: - # return subscript - return graph + if graph_index in self.graphs: + return self.graphs[graph_index] # not found return None diff --git a/python/gui_manager.py b/python/gui_manager.py index 63396055f141675aaf8226f684d972941b9ff3b1..4254bf4277a3d90477305c33f49166c799bf1247 100644 --- a/python/gui_manager.py +++ b/python/gui_manager.py @@ -631,8 +631,8 @@ class GUIManager(QObject): else: # accept request - status, graphs = mp_code_io_manager.read_generated_json( - generated_json_text) + status, global_view_specification, graphs = \ + mp_code_io_manager.read_generated_json(generated_json_text) if status: # log failure diff --git a/python/mp_code_io_manager.py b/python/mp_code_io_manager.py index 7b3f20b846e20828929f6972772dd30ea9d401dc..5c691189e34febbbf437f8ecd845bb5f7eaf464b 100644 --- a/python/mp_code_io_manager.py +++ b/python/mp_code_io_manager.py @@ -31,8 +31,9 @@ read_generated_json(generated_json): import_gry_file(gry_filename): import JGF Gryphon file. Return (status, metadata fields and graphs) -NOTE: RIGAL functions read/write in same directory, so we change to the -scratch RIGAL work directory to work, then move back when done. +export_gry_file(gry_filename, mp_code_text, scope, + selected_index, scale, x_slider, y_slider, graphs): + export JGF Gryphon file. Return status """ # read MP Code file. Return (status, mp_code_text) @@ -69,7 +70,8 @@ def save_mp_code_file(mp_code_text, mp_code_filename): # NOTE: per request, removes underscores from node label def read_generated_json(generated_json_text): - graphs = list() + graphs = dict() + global_view_specification = dict() # zz not populated yet recommended_node_width = settings["node_width"] recommended_node_height = settings["node_height"] graph_h_spacing = settings["graph_h_spacing"] @@ -87,10 +89,11 @@ def read_generated_json(generated_json_text): if "GLOBAL" in generated_json: print("Implementation TBD for GLOBAL:%s"%generated_json["GLOBAL"]) - i = 1 + trace_index = 1 for trace in generated_json["traces"]: nodes = list() edges = list() + trace_views = dict() # build graph from trace # item 0: graph mark, "U" or "M" @@ -101,25 +104,25 @@ def read_generated_json(generated_json_text): # item 2: event list of nodes node_lookup = dict() # helper for connecting edges - json_nodes = trace[2] + json_nodes = trace[2] # event list y_extra_say = 0 - for json_node in json_nodes: + for json_node in json_nodes: # event in event list - # prepare label without underscores - label = json_node[0].replace('_'," ") + # prepare event name without underscores + event_name = json_node[0].replace('_'," ") - # get x and y, not available in older JSON + # compute x and y x=json_node[3] * graph_h_spacing + recommended_node_width/2 y=json_node[4] * graph_v_spacing + recommended_node_height/2 if json_node[1] == "T": # SAY # spread out SAY boxes more - y_extra_say += 22 * (len(label)//20) + y_extra_say += 22 * (len(event_name)//20) y += y_extra_say # y=json_node[4]*1.8*graph_v_spacing+recommended_node_height # node_id, type, name, x, y, hide, collapse, collapse_below node = Node("%s"%json_node[2], json_node[1], - label, x, y, False, False, False) + event_name, x, y, False, False, False) nodes.append(node) node_lookup[node.node_id] = node @@ -139,30 +142,39 @@ def read_generated_json(generated_json_text): "%s"%json_edge[0], "", node_lookup)) - # items 5...n-1: the user-defined named relations - for user_defined_json_edges in trace[5:]: - if isinstance(user_defined_json_edges, list): - label = user_defined_json_edges[0] - for json_edge in user_defined_json_edges[1:]: - # source_id, relation, target_id, label + # items 5...: list of user-defined named relations and dict of views + for type_specific_element in trace[5:]: + if isinstance(type_specific_element, list): + relation_name = type_specific_element[0] + for json_edge in type_specific_element[1:]: + # source_id, relation, target_id, relation_name edges.append(Edge("%s"%json_edge[0], "USER_DEFINED", - "%s"%json_edge[1], label, + "%s"%json_edge[1], relation_name, node_lookup)) - elif isinstance(user_defined_json_edges, dict): + elif isinstance(type_specific_element, dict): # VIEWS - for key, value in user_defined_json_edges.items(): + for key, value in type_specific_element.items(): if value: + views[key] = value print("Implementation TBD for %s:%s"%(key, value)) - # build graph item from this - graph_item = GraphItem(i, graph_mark, trace_probability, - nodes, edges) - graphs.append(graph_item) - - i += 1 - - return ("", graphs) +# # build graph item from this +# graph_item = GraphItem(trace_index, graph_mark, trace_probability, +# nodes, edges) + graph = {"index":trace_index, + "mark":graph_mark, + "probability":trace_probability, + "nodes":nodes, + "edges":edges, + "trace_views":trace_views, + "graph_item":None # calculated just-in-time + } + + graphs[trace_index] = graph + trace_index += 1 + + return ("", global_view_specification, graphs) except Exception as e: print("Error reading generated JSON text: %s" % (repr(e)))