2222
2323from PySide6 import QtCore , QtWidgets , QtGui
2424
25+ from mapclient .core .workflow .layouts .forcedirected import ForceDirectedLayout
26+ from mapclient .core .workflow .layouts .springforce import SpringForce
2527from mapclient .core .workflow .workflowutils import revert_parameterised_position
2628from mapclient .mountpoints .workflowstep import workflowStepFactory
2729from mapclient .core .workflow .workflowscene import MetaStep
3133
3234logger = logging .getLogger ()
3335
36+ # This is the threshold for the stability of the layout. If the change in position of all nodes is less than this value,
37+ # the layout is considered stable and the animation stops.
38+ STABILITY_THRESHOLD = 0.05
39+ # The number of consecutive stable frames before the layout is considered stable.
40+ CONSECUTIVE_STABLE_FRAMES = 5
41+
3442
3543class WorkflowGraphicsView (QtWidgets .QGraphicsView ):
3644
@@ -48,6 +56,14 @@ def __init__(self, parent=None):
4856 self ._graphics_scale_factor = 1.0
4957 self ._margin = 10
5058
59+ self ._layout_engine = None
60+ self ._layout_timer = QtCore .QTimer ()
61+ self ._layout_timer .timeout .connect (self ._animation_step )
62+ self ._layout_iteration_count = 0
63+ self ._layout_initial_positions = None
64+ self ._layout_previous_positions = None
65+ self ._layout_stability_counter = 0
66+
5167 self ._undoStack = None
5268 self ._location = ''
5369 self ._showStepNames = True
@@ -602,3 +618,150 @@ def scale_workflow(self, sf_x, sf_y):
602618 if x != item .x () or y != item .y ():
603619 self ._undoStack .push (CommandMove (item , item .pos (), QtCore .QPointF (x , y )))
604620 self ._undoStack .endMacro ()
621+
622+ def layout_workflow (self , layout_algorithm , animate = True ):
623+ sceneRect = self .sceneRect ()
624+ core_scene = self .scene ().workflowScene ()
625+
626+ graph , graph_edges , reverse_graph = core_scene .graph ()
627+ adjacent_graph = {}
628+ vertices = []
629+
630+ unique_meta_steps = set ()
631+ for node , targets in graph .items ():
632+ unique_meta_steps .add (node )
633+ unique_meta_steps .update (targets )
634+
635+ vertices .append (node )
636+ adjacent_graph [node ] = graph [node ]
637+
638+ node_definitions = {}
639+ for node in unique_meta_steps :
640+ node_ports = _determine_input_output_ports (node )
641+ node_definitions [node .getIdentifier ()] = {
642+ 'name' : node .getName (),
643+ 'position' : (node .getPos ().x (), node .getPos ().y ()),
644+ 'inputs' : node_ports ['inputs' ],
645+ 'outputs' : node_ports ['outputs' ],
646+ }
647+
648+ edge_definitions = [
649+ {'from' : edge ['from' ].getIdentifier (), 'from_port' : edge ['from_port' ],
650+ 'to' : edge ['to' ].getIdentifier (), 'to_port' : edge ['to_port' ]}
651+ for edge in graph_edges
652+ ]
653+
654+ for node in reverse_graph :
655+ if node not in adjacent_graph :
656+ vertices .append (node )
657+ adjacent_graph [node ] = []
658+
659+ adjacent_graph [node ] = list (set (adjacent_graph [node ] + reverse_graph [node ]))
660+
661+ all_set = set (adjacent_graph .keys ())
662+ non_adjacent_graph = {}
663+ for node in adjacent_graph :
664+ adjacent_set = set (adjacent_graph [node ])
665+ non_adjacent_set = all_set - adjacent_set - {node }
666+ non_adjacent_graph [node ] = list (non_adjacent_set )
667+
668+ dataset = {
669+ 'vertices' : vertices ,
670+ 'adjacent' : adjacent_graph ,
671+ 'non_adjacent' : non_adjacent_graph
672+ }
673+
674+ if layout_algorithm == 'force_directed' :
675+ self ._layout_engine = ForceDirectedLayout (node_definitions , edge_definitions , sceneRect .width ())
676+ elif layout_algorithm == 'spring_force' :
677+ self ._layout_engine = SpringForce (dataset , iterations = 100 , target_node_speed = 0.01 )
678+
679+ self ._layout_iteration_count = 0
680+ self ._layout_initial_positions = {k : QtCore .QPoint (v [0 ], v [1 ]) for k , v in self ._layout_engine .positions ().items ()}
681+ self ._layout_previous_positions = {k : QtCore .QPoint (v [0 ], v [1 ]) for k , v in self ._layout_engine .positions ().items ()}
682+ if animate :
683+ self ._undoStack .beginMacro ('Move Step(s)' )
684+ self ._layout_timer .start (16 )
685+ else :
686+ self ._layout_workflow ()
687+
688+ def _move_nodes (self , positions ):
689+ for item in self .items ():
690+ if item .type () == Node .Type :
691+ item_identifier = item .metaItem ().getIdentifier ()
692+ if item_identifier in positions :
693+ self ._undoStack .push (CommandMove (item , self ._layout_initial_positions [item_identifier ], positions [item_identifier ]))
694+
695+ def _layout_workflow (self ):
696+ while self ._layout_iteration_count < self ._layout_engine .max_iterations ():
697+ if hasattr (self ._layout_engine , 'update_layout' ):
698+ damping = 1.0 - (self ._layout_iteration_count / self ._layout_engine .max_iterations ())
699+ self ._layout_engine .update_layout (damping )
700+ elif hasattr (self ._layout_engine , 'spring_layout' ):
701+ self ._layout_engine .spring_layout (c1 = 2.0 , c2 = 10.0 , c3 = 1000000 , c4 = 1 , return_after = 1 )
702+
703+ positions = {k : QtCore .QPoint (v [0 ], v [1 ]) for k , v in self ._layout_engine .positions ().items ()}
704+ self ._check_stability (positions )
705+
706+ self ._layout_iteration_count += 1
707+
708+ positions = {k : QtCore .QPoint (v [0 ], v [1 ]) for k , v in self ._layout_engine .positions ().items ()}
709+
710+ self ._undoStack .beginMacro ('Move Step(s)' )
711+ self ._move_nodes (positions )
712+ self ._undoStack .endMacro ()
713+
714+ def _check_stability (self , positions ):
715+ total_movement_sq = _calculate_total_movement (self ._layout_previous_positions , positions )
716+ self ._layout_previous_positions = positions
717+ if total_movement_sq < STABILITY_THRESHOLD :
718+ self ._layout_stability_counter += 1
719+ else :
720+ # If there's any significant movement, reset the counter
721+ self ._layout_stability_counter = 0
722+
723+ if self ._layout_stability_counter >= CONSECUTIVE_STABLE_FRAMES :
724+ self ._layout_iteration_count = self ._layout_engine .max_iterations ()
725+
726+ def _animation_step (self ):
727+ if self ._layout_iteration_count >= self ._layout_engine .max_iterations ():
728+ self ._undoStack .endMacro ()
729+ self ._layout_timer .stop ()
730+ return
731+
732+ if hasattr (self ._layout_engine , 'update_layout' ):
733+ damping = 1.0 - (self ._layout_iteration_count / self ._layout_engine .max_iterations ())
734+ self ._layout_engine .update_layout (damping )
735+ elif hasattr (self ._layout_engine , 'spring_layout' ):
736+ self ._layout_engine .spring_layout (c1 = 2.0 , c2 = 10.0 , c3 = 1000000 , c4 = 1 , return_after = 1 )
737+
738+ positions = {k : QtCore .QPoint (v [0 ], v [1 ]) for k , v in self ._layout_engine .positions ().items ()}
739+ self ._move_nodes (positions )
740+ self ._check_stability (positions )
741+ self ._layout_iteration_count += 1
742+
743+
744+ def _calculate_total_movement (previous_positions , current_positions ):
745+ total_movement_sq = 0
746+ for identifier , prev_pos in previous_positions .items ():
747+ curr_pos = current_positions [identifier ]
748+
749+ dx = curr_pos .x () - prev_pos .x ()
750+ dy = curr_pos .y () - prev_pos .y ()
751+ total_movement_sq += dx * dx + dy * dy
752+
753+ return total_movement_sq
754+
755+
756+ def _determine_input_output_ports (meta_step ):
757+ """
758+ Determine the input and output ports for a step.
759+ :param meta_step: The step to determine the ports for.
760+ :return: A list of input and output ports.
761+ """
762+ step = meta_step .getStep ()
763+ step_ports = step .getPorts ()
764+ uses_ports = [port for port in step_ports if port .has_uses ()]
765+ provides_ports = [port for port in step_ports if port .has_provides ()]
766+
767+ return {'inputs' : len (uses_ports ), 'outputs' : len (provides_ports )}
0 commit comments