diff --git a/resources/graphs/basic_graph/graph.py b/resources/graphs/basic_graph/graph.py index 474209a33dc6fd02d2e06221d880a7e96a439dfb..fb6473e8f188912b99b6a9b19e443b051918fc26 100644 --- a/resources/graphs/basic_graph/graph.py +++ b/resources/graphs/basic_graph/graph.py @@ -216,14 +216,36 @@ class BasicGraphDisplay(): """ Assigns Graph mapping coordinates for all nodes. Accomplishes this with random assignment, on a grid of 100 by 100, such that no nodes should overlap. + + If graph is of type that has "start" or "end" nodes, then it's modified to be slightly more organized. + Accomplishes this by putting initial/start nodes on the far left, and final nodes on far right. + All other node coordinates are randomly assigned to the center. """ + # Get "start" and "end" node lists. + start_node_list, end_node_list = self._get_start_and_end_node_lists() + + # Get all possible node position coordinates. max_direction = 100 possible_positions = [] - # Create list of "possible_positions". Should be a list of every possible (x,y) coordinate pairing. - for x_index in range(int(max_direction)): - for y_index in range(int(max_direction)): - possible_positions.append((x_index, y_index)) + # If start or end node lists exist, we want to limit coordinate positioning slightly. + if len(start_node_list) != 0 or len(end_node_list) != 0: + logger.info('start or end node lists is greater than length 0.') + + # Create list of "possible_positions". Should exclude far left or far right coordinates. + for x_index in range(int(max_direction)): + # We want to limit x-coords, preventing random assignment to x-coords less than 10 or greater than 90. + if x_index > 10 and x_index < 90: + # Given x-coord is between values we want. Proceed. + for y_index in range(int(max_direction)): + possible_positions.append((x_index, y_index)) + + else: + logger.info('Start or end node lists is length 0.') + # Create list of "possible_positions". Should be a list of every possible (x,y) coordinate pairing. + for x_index in range(int(max_direction)): + for y_index in range(int(max_direction)): + possible_positions.append((x_index, y_index)) # Values to remember extreme coord values, as well as associated nodes. farthest_x = (0, None) @@ -231,24 +253,84 @@ class BasicGraphDisplay(): shortest_x = (100, None) shortest_y = (100, None) - # Now go through all nodes, assigning one to a random position. - # As long as there are less than 1000 nodes, this should work without issues. + # Loop through all "end" states, assigning y-coords based on a divided grid setup. + # Grids are from top to bottom, to evenly distribute space to all final nodes. + # Final states are done first, in case a node is both final and initial. In this case, we want it on the left. + # Going first means that the initial state logic will override the final state logic, putting it left like we + # want. + end_node_count = len(end_node_list) + if end_node_count > 0: + state_index = 1 + grid_length = max_direction / end_node_count + for end_state in end_node_list: + # Get grid values. + current_grid_end = grid_length * state_index + current_grid_start = current_grid_end - grid_length + state_index += 1 + + # Assign node to calculated grid. + x_coord = 95 + y_coord = int((current_grid_start + current_grid_end) / 2) + self._parent._nodes[end_state.get_name()].x_coord = x_coord + self._parent._nodes[end_state.get_name()].y_coord = y_coord + + # Record extreme positions. + if x_coord > farthest_x[0]: + farthest_x = (x_coord, end_state) + if x_coord < shortest_x[0]: + shortest_x = (x_coord, end_state) + if y_coord > farthest_y[0]: + farthest_y = (y_coord, end_state) + if y_coord < shortest_y[0]: + shortest_y = (y_coord, end_state) + + # Loop through all initial states, assigning y-coords based on a divided grid setup. + # Grids are from top to bottom, to evenly distribute space to all start nodes. + start_node_count = len(start_node_list) + if start_node_count > 0: + state_index = 1 + grid_length = max_direction / start_node_count + for start_node in start_node_list: + # Get grid values. + current_grid_end = grid_length * state_index + current_grid_start = current_grid_end - grid_length + state_index += 1 + + # Assign node to calculated grid. + x_coord = 5 + y_coord = int((current_grid_start + current_grid_end) / 2) + self._parent._nodes[start_node.get_name()].x_coord = x_coord + self._parent._nodes[start_node.get_name()].y_coord = y_coord + + # Record extreme positions. + if x_coord > farthest_x[0]: + farthest_x = (x_coord, start_node) + if x_coord < shortest_x[0]: + shortest_x = (x_coord, start_node) + if y_coord > farthest_y[0]: + farthest_y = (y_coord, start_node) + if y_coord < shortest_y[0]: + shortest_y = (y_coord, start_node) + + # Loop through all currently unassigned nodes and assign coordinates. for node in self._parent.nodes.all().values(): - # Get a random position. - index = randint(0, len(possible_positions) - 1) - position = possible_positions.pop(index) - self._parent._nodes[node.get_name()].x_coord = position[0] - self._parent._nodes[node.get_name()].y_coord = position[1] - - # Record extreme positions. - if position[0] > farthest_x[0]: - farthest_x = (position[0], node) - if position[0] < shortest_x[0]: - shortest_x = (position[0], node) - if position[1] > farthest_y[0]: - farthest_y = (position[1], node) - if position[1] < shortest_y[0]: - shortest_y = (position[1], node) + # Make sure node is not already assigned coordinates. + if node.x_coord is None or node.y_coord is None: + # Get a random position. + index = randint(0, len(possible_positions)) + position = possible_positions.pop(index) + self._parent._nodes[node.get_name()].x_coord = position[0] + self._parent._nodes[node.get_name()].y_coord = position[1] + + # Record extreme positions. + if position[0] > farthest_x[0]: + farthest_x = (position[0], node) + if position[0] < shortest_x[0]: + shortest_x = (position[0], node) + if position[1] > farthest_y[0]: + farthest_y = (position[1], node) + if position[1] < shortest_y[0]: + shortest_y = (position[1], node) # Now that all nodes are mapped, adjust extreme node coords, so that all graphs will be roughly the same size. if shortest_x[0] > 10: @@ -279,6 +361,14 @@ class BasicGraphDisplay(): self._parent._edges[edge.get_name()].y1_coord = node_1.y_coord self._parent._edges[edge.get_name()].y2_coord = node_2.y_coord + def _get_start_and_end_node_lists(self): + """ + Returns lists of "start" and "end" nodes in graph. + + "Basic" graphs do not have these node types so it returns empty lists. + """ + return ([], []) + def _create_node_mapping(self): """ Maps graph nodes to values that the Visual Mapping can understand. diff --git a/resources/graphs/directed_graph/graph.py b/resources/graphs/directed_graph/graph.py index 130297829ff0c3129a7666d350232abda7820834..a70c9ca68dc69148a7299b566bffc36fe6c20db5 100644 --- a/resources/graphs/directed_graph/graph.py +++ b/resources/graphs/directed_graph/graph.py @@ -11,6 +11,7 @@ Parent structure containing various Nodes and Edges. # System Imports. import math +from random import randint # User Class Imports. from .components import DirectedEdge, DirectedNode @@ -63,6 +64,30 @@ class DirectedGraphDisplay(BasicGraphDisplay): # Get calling parent. self._parent = parent + def _get_start_and_end_node_lists(self): + """ + Returns lists of "start" and "end" nodes in graph. + + For a directed graph, "start" nodes are all nodes with only outgoing edges. + "End" nodes are all nodes with only incoming edges. + """ + start_node_list = [] + end_node_list = [] + + # Find all nodes that only have inward edges. + for node in self._parent._nodes.values(): + # Check that node has at least 1 edge and no edges are outgoing. + if node.get_edge_count() > 0 and len(node.get_outgoing_nodes()) == 0: + end_node_list.append(node) + + # Find all nodes that only have outward edges. + for node in self._parent._nodes.values(): + # Check that node has at least 1 edge and no edges are incoming. + if node.get_edge_count() > 0 and len(node.get_incoming_nodes()) == 0: + start_node_list.append(node) + + return (start_node_list, end_node_list) + def _create_additional_edge_mappings(self): """ Creates additional visual edge direction "pointer" mappings for all edge. diff --git a/resources/graphs/state_machine/graph.py b/resources/graphs/state_machine/graph.py index 3b7ad1c781c739e2f3416199c7a2d582459a5668..3ba6d4b086a02372f65e83685bcd7188722a734b 100644 --- a/resources/graphs/state_machine/graph.py +++ b/resources/graphs/state_machine/graph.py @@ -67,129 +67,14 @@ class StateMachineGraphDisplay(DirectedGraphDisplay): # Get calling parent. self._parent = parent - def _assign_node_coordinates(self): + def _get_start_and_end_node_lists(self): """ - Assigns Graph mapping coordinates for all nodes. - Still mostly random coordinate assignment (just like the basic graph). However, slightly more organized. - Accomplishes this by putting initial/start nodes on the far left, and final nodes on far right. - All other node coordinates are randomly assigned to the center. + Returns lists of "start" and "end" nodes in graph. + + For a State Machine graph, "start" nodes are all nodes marked with an "initial" state. + "End" nodes are all nodes marked with a "final" state. """ - max_direction = 100 - possible_positions = [] - - # Create list of "possible_positions". Should be a list of every possible (x,y) coordinate pairing. - for x_index in range(int(max_direction)): - # We want to limit x-coords, preventing random assignment to x-coords less than 10 or greater than 90. - if x_index > 10 and x_index < 90: - # Given x-coord is between values we want. Proceed. - for y_index in range(int(max_direction)): - possible_positions.append((x_index, y_index)) - - # Values to remember extreme coord values, as well as associated nodes. - farthest_x = (0, None) - farthest_y = (0, None) - shortest_x = (100, None) - shortest_y = (100, None) - - # Loop through all final states, assigning y-coords based on a divided grid setup. - # Grids are from top to bottom, to evenly distribute space to all final nodes. - # Final states are done first, in case a node is both final and initial. In this case, we want it on the left. - # Going first means that the intial state logic will override the final state logic, putting it left like we - # want. - final_state_count = len(self._parent._final_states) - state_index = 1 - grid_length = max_direction / final_state_count - for final_state in self._parent._final_states.values(): - # Get grid values. - current_grid_end = grid_length * state_index - current_grid_start = current_grid_end - grid_length - state_index += 1 - - # Assign node to calculated grid. - x_coord = 95 - y_coord = int((current_grid_start + current_grid_end) / 2) - self._parent._nodes[final_state.get_name()].x_coord = x_coord - self._parent._nodes[final_state.get_name()].y_coord = y_coord - - # Record extreme positions. - if x_coord > farthest_x[0]: - farthest_x = (x_coord, final_state) - if x_coord < shortest_x[0]: - shortest_x = (x_coord, final_state) - if y_coord > farthest_y[0]: - farthest_y = (y_coord, final_state) - if y_coord < shortest_y[0]: - shortest_y = (y_coord, final_state) - - # Loop through all initial states, assigning y-coords based on a divided grid setup. - # Grids are from top to bottom, to evenly distribute space to all start nodes. - initial_state_count = len(self._parent._initial_states) - state_index = 1 - grid_length = max_direction / initial_state_count - for initial_state in self._parent._initial_states.values(): - # Get grid values. - current_grid_end = grid_length * state_index - current_grid_start = current_grid_end - grid_length - state_index += 1 - - # Assign node to calculated grid. - x_coord = 5 - y_coord = int((current_grid_start + current_grid_end) / 2) - self._parent._nodes[initial_state.get_name()].x_coord = x_coord - self._parent._nodes[initial_state.get_name()].y_coord = y_coord - - # Record extreme positions. - if x_coord > farthest_x[0]: - farthest_x = (x_coord, initial_state) - if x_coord < shortest_x[0]: - shortest_x = (x_coord, initial_state) - if y_coord > farthest_y[0]: - farthest_y = (y_coord, initial_state) - if y_coord < shortest_y[0]: - shortest_y = (y_coord, initial_state) - - # Now loop through all other nodes and assign coordinates. - for node in self._parent.nodes.all().values(): - # Make sure node is not already assigned coordinates. - if node.x_coord is None or node.y_coord is None: - # Get a random position. - index = randint(0, len(possible_positions)) - position = possible_positions.pop(index) - self._parent._nodes[node.get_name()].x_coord = position[0] - self._parent._nodes[node.get_name()].y_coord = position[1] - - # Record extreme positions. - if position[0] > farthest_x[0]: - farthest_x = (position[0], node) - if position[0] < shortest_x[0]: - shortest_x = (position[0], node) - if position[1] > farthest_y[0]: - farthest_y = (position[1], node) - if position[1] < shortest_y[0]: - shortest_y = (position[1], node) - - # Now that all nodes are mapped, adjust extreme node coords, so that all graphs will be roughly the same size. - if shortest_x[0] > 10: - shortest_x[1].x_coord = 10 - if shortest_y[0] > 10: - shortest_y[1].y_coord = 10 - if farthest_x[0] < 90: - farthest_x[1].x_coord = 90 - if farthest_y[0] < 90: - farthest_y[1].y_coord = 90 - - # Now go through all edges and do similar. - for edge in self._parent.edges.all().values(): - node_list = edge.get_nodes() - - node_1 = node_list['tail_node'] - node_2 = node_list['head_node'] - - # Assign edge coordinates. - self._parent._edges[edge.get_name()].x1_coord = node_1.x_coord - self._parent._edges[edge.get_name()].x2_coord = node_2.x_coord - self._parent._edges[edge.get_name()].y1_coord = node_1.y_coord - self._parent._edges[edge.get_name()].y2_coord = node_2.y_coord + return (self._parent._initial_states.values(), self._parent._final_states.values()) def _create_node_hover_text(self): """