package de.unisiegen.gtitool.ui.utils; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import org.jgraph.graph.GraphConstants; import de.unisiegen.gtitool.core.entities.State; import de.unisiegen.gtitool.core.entities.Transition; import de.unisiegen.gtitool.ui.jgraph.DefaultStateView; import de.unisiegen.gtitool.ui.jgraph.StateView; import de.unisiegen.gtitool.ui.model.DefaultMachineModel; import de.unisiegen.gtitool.ui.redoundo.MultiItem; import de.unisiegen.gtitool.ui.redoundo.RedoUndoHandler; import de.unisiegen.gtitool.ui.redoundo.StateMovedItem; /** * This class manages the layout of the graph. * * @author Benjamin Mies * @version $Id$ */ public final class LayoutManager { /** * The already fixed nodes. */ private ArrayList < DefaultStateView > fixedNodes = new ArrayList < DefaultStateView > (); /** * List of all {@link DefaultStateView} groups. */ private ArrayList < ArrayList < DefaultStateView >> groups = new ArrayList < ArrayList < DefaultStateView > > (); /** * The actual min costs. */ private int minCosts = Integer.MAX_VALUE; /** * The {@link DefaultMachineModel}. */ private DefaultMachineModel model; /** * The actual first node to swap. */ private DefaultStateView swapNode0; /** * The actual second node to swap. */ private DefaultStateView swapNode1; /** * The {@link RedoUndoHandler}. */ private RedoUndoHandler redoUndoHandler; /** * The {@link MultiItem} for redo/undo. */ private MultiItem item; /** * Allocates a new {@link LayoutManager}. * * @param model The {@link DefaultMachineModel}. * @param redoUndoHandler The {@link RedoUndoHandler}. */ public LayoutManager ( DefaultMachineModel model, RedoUndoHandler redoUndoHandler ) { this.model = model; this.redoUndoHandler = redoUndoHandler; } /** * Calculates the cuts of the graph. * * @return The count of the cuts. */ private final int calculateAllCuts () { int count = 0; for ( int i = 0 ; i < this.groups.size () ; i++ ) { count += calculateCuts ( i ); } return count; } /** * Calculates the cuts between the given index and the groups below. * * @param index The index of the given group. * @return The count of cuts. */ private final int calculateCuts ( final int index ) { int count = 0; for ( int i = 0 ; i <= index ; i++ ) { for ( DefaultStateView current : this.groups.get ( i ) ) { for ( Transition transition : current.getState ().getTransitionBegin () ) { if ( isCut ( transition.getStateEnd (), index ) ) { count++ ; } } for ( Transition transition : current.getState ().getTransitionEnd () ) { if ( isCut ( transition.getStateBegin (), index ) ) { count++ ; } } } } return count; } /** * Layouts the states into a grid to perform next layout steps. * * @param states List with all states of the machine. */ private final void createGrid ( ArrayList < DefaultStateView > states ) { int insets = 5 + Math.max ( StateView.START_OFFSET, StateView.LOOP_TRANSITION_OFFSET ); int diff = 50; int xPosition = insets; int yPosition = insets + diff; int xStartPosition = xPosition; int xSpace = 100; int nextRowOffset = 200; if ( states.size () > 0 ) { Rectangle2D rect = GraphConstants.getBounds ( states.get ( 0 ) .getAttributes () ); xSpace = ( int ) rect.getWidth () + diff; } ArrayList < DefaultStateView > group = new ArrayList < DefaultStateView > (); int rowSize = ( int ) Math.ceil ( Math.sqrt ( this.model .getStateViewList ().size () ) ); int count = 0; int pos = 0; for ( DefaultStateView current : states ) { if ( count != 0 ) { if ( ( count % rowSize ) == 0 ) { this.groups.add ( group ); pos = 0; group = new ArrayList < DefaultStateView > (); xPosition = xStartPosition; yPosition += nextRowOffset; } else { xPosition += xSpace; } } group.add ( current ); if ( this.item != null ) { // correct the x position if the state is a start state int tmpPositionX = xPosition; if ( current.getState ().isStartState () ) { tmpPositionX -= StateView.START_OFFSET; } // correct the y position if the state contains a loop transition int tmpPositionY = yPosition; if ( current.getState ().isLoopTransition () ) { tmpPositionY -= StateView.LOOP_TRANSITION_OFFSET; } if ( ( pos % 2 ) != 0 ) { if ( ( current.getPositionX () != tmpPositionX ) || ( current.getPositionY () != tmpPositionY + diff ) ) { this.item.addItem ( new StateMovedItem ( this.model, current, current.getPositionX (), current.getPositionY (), tmpPositionX, tmpPositionY + diff ) ); current.move ( tmpPositionX, tmpPositionY + diff ); } } else { if ( ( current.getPositionX () != tmpPositionX ) || ( current.getPositionY () != tmpPositionY - diff ) ) { this.item.addItem ( new StateMovedItem ( this.model, current, current.getPositionX (), current.getPositionY (), tmpPositionX, tmpPositionY - diff ) ); current.move ( tmpPositionX, tmpPositionY - diff ); } } } count++ ; pos++ ; } this.groups.add ( group ); } /** * Performs the graph layout. */ public final void doLayout () { prelayout (); doLayoutInternal (); finishLayout (); this.model.getGraphModel ().cellsChanged ( this.model.getStateViewList ().toArray () ); } /** * Performs the internal layout action. */ private final void doLayoutInternal () { for ( int index = 0 ; index < this.groups.size () - 1 ; index++ ) { while ( !this.fixedNodes.containsAll ( this.groups.get ( index + 1 ) ) ) { findNodesToSwap ( index ); if ( this.swapNode0 == null ) { continue; } if ( calculateAllCuts () > this.minCosts ) { swap ( index, this.swapNode0, index + 1, this.swapNode1 ); } this.fixedNodes.add ( this.swapNode0 ); this.fixedNodes.add ( this.swapNode1 ); this.minCosts = Integer.MAX_VALUE; this.swapNode0 = null; this.swapNode1 = null; } this.fixedNodes.clear (); } } /** * Finds the best nodes to swap. * * @param index The index of the actual group. */ private final void findNodesToSwap ( int index ) { ArrayList < DefaultStateView > group0 = this.groups.get ( index ); ArrayList < DefaultStateView > group1 = this.groups.get ( index + 1 ); for ( int i = 0 ; i < group0.size () ; i++ ) { DefaultStateView node0 = group0.get ( i ); if ( this.fixedNodes.contains ( node0 ) ) { continue; } for ( int j = 0 ; j < group1.size () ; j++ ) { DefaultStateView node1 = group1.get ( j ); if ( this.fixedNodes.contains ( node1 ) ) { continue; } swap ( index, node0, index + 1, node1 ); int costs = calculateAllCuts (); if ( costs < this.minCosts ) { this.minCosts = costs; this.swapNode0 = node0; this.swapNode1 = node1; } swap ( index, node1, index + 1, node0 ); } } } /** * Layouts the graph into a grid. */ private final void finishLayout () { ArrayList < DefaultStateView > states = new ArrayList < DefaultStateView > (); for ( ArrayList < DefaultStateView > current : this.groups ) { states.addAll ( current ); } this.item = new MultiItem (); createGrid ( states ); if ( ( this.redoUndoHandler != null ) && ( this.item.size () > 0 ) ) { this.redoUndoHandler.addItem ( this.item ); } this.item = null; } /** * Checks if there is a cut for the given {@link State}. * * @param state The {@link State}. * @param groupIndex The actual group index. * @return True if there is a cut, else false. */ private final boolean isCut ( State state, int groupIndex ) { for ( int i = groupIndex + 1 ; i < this.groups.size () ; i++ ) { for ( DefaultStateView current : this.groups.get ( i ) ) { if ( current.getState ().equals ( state ) ) { return true; } } } return false; } /** * Layouts the graph into a grid. */ private final void prelayout () { createGrid ( this.model.getStateViewList () ); } /** * Resorts the groups. Not used in the moment. */ @SuppressWarnings ( "unused" ) private void resortGroups () { ArrayList < ArrayList < DefaultStateView > > oldGroups = this.groups; this.groups = new ArrayList < ArrayList < DefaultStateView > > (); for ( int j = 0 ; j < oldGroups.get ( 0 ).size () ; j++ ) { ArrayList < DefaultStateView > group = new ArrayList < DefaultStateView > (); for ( int i = 0 ; i < oldGroups.size () ; i++ ) { if ( oldGroups.get ( i ).size () > j ) { DefaultStateView node = oldGroups.get ( i ).get ( j ); group.add ( node ); } } this.groups.add ( group ); } } /** * Swaps the {@link DefaultStateView}. * * @param groupIndex0 The group index of the first {@link DefaultStateView}. * @param stateView0 The first {@link DefaultStateView}. * @param groupIndex1 The group index of the second {@link DefaultStateView}. * @param stateView1 The second {@link DefaultStateView}. */ private final void swap ( int groupIndex0, DefaultStateView stateView0, int groupIndex1, DefaultStateView stateView1 ) { ArrayList < DefaultStateView > list0 = this.groups.get ( groupIndex0 ); ArrayList < DefaultStateView > list1 = this.groups.get ( groupIndex1 ); int index0 = list0.indexOf ( stateView0 ); int index1 = list1.indexOf ( stateView1 ); list0.add ( index0, stateView1 ); list0.remove ( stateView0 ); list1.add ( index1, stateView0 ); list1.remove ( stateView1 ); } }