All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jgraph.layout.GEMLayoutAlgorithm Maven / Gradle / Ivy

There is a newer version: 0.4.7
Show newest version
package org.jgraph.layout;

import org.jgraph.JGraph;
import org.jgraph.event.GraphModelEvent;
import org.jgraph.event.GraphModelListener;
import org.jgraph.graph.*;
import org.jgraph.utils.MathExtensions;

import java.awt.*;
import java.awt.geom.Point2D;
import java.util.*;

/**
 * 

GEM Layout Algorithm

*

* Based on the work of Arne Frick, Andreas Ludwig, Heiko Mehldau: * "A Fast Adaptive Layout Algorithm for Undirected Graphs"; * Extended Abstract and System Demonstration; Faculty of Informatik of * the University Karlsruhe; 1994 *

* This Algorithm works by giving every cell a position and a temperature. * Then for every cell forces are computed. Every other cell repulses the * actual calculated cell away. On the other hand, cells, connected by edges * are attracted, until a minimum distance is reached. The result of this * forces is a move of the position of the actual cell in the direction of * the force and with the length of the temperature. Then the temperature * will be decreased, if the last impulse and the current impulse looks like a * rotation or a oscillation. this is done for every cell until the temperature * of all cells or the average of the temperature of every cell is until a * given minimum value or a maximum of rounds is reached. * @author winkler */ public class GEMLayoutAlgorithm implements LayoutAlgorithm, GraphModelListener { /** * Key used on every cell. This key indicates that in the cell are * attributes stored by this algorithm. The algorithm itself never asks for * this key. This is for others developers only, using this algorithm, to * find out, where sometimes approaching attributes come from. */ public final static String KEY_CAPTION = "GEM-TEMPORARY-DATA"; /** * Key used on every cell. Under this key every cell stores temporary the * temperature of itself. Temperature is a indicator how far a cell can move * and all temperatures together indicating how long the algorithm will run. */ public final static String KEY_TEMPERATURE = "Temperature"; /** * Key used on every cell. Under this key every cell stores temporary the * current force impulse affecting the cell. */ public final static String KEY_CURRENT_IMPULSE = "Current_Impulse"; /** * Key used on every cell. Under this key every cell stores temporary the * last impulse. This is the value of the previous * {@link #KEY_CURRENT_IMPULSE}. */ public final static String KEY_LAST_IMPULSE = "Last_Impulse"; /** * Key used on every cell. Under this key every cell stores the temporary * position on the display, while the calculation is running. This makes * the algorithm faster, than asking the Cell everytime for its position. * So the algorithm can anytime be canceled, whithout changing something. */ public final static String KEY_POSITION = "Position"; /** * Key used on every cell. Under this key every cell stores the temporary * skew gauge. This value is for punish rotations of the cells. */ public final static String KEY_SKEWGAUGE = "Skew_Gauge"; /** * Key used on every Cell. Under this key every cell stores the cells, * that are only one edge away from it. The relatives are stored, after the * first time, they are desired and calculated by * {@link #getRelatives(CellView)}. */ public final static String KEY_RELATIVES = "Relatives"; /** * Key used on every Cell. This indicates a weight for the number of edges * received by {@link #getNodeWeight(CellView)} */ public final static String KEY_MASSINDEX = "Mass_Index"; /** * Key used only with clusters. Under this key a cluster has an ArrayList. * This list is filled with the clustered vertices. * @see #clusterGraph() */ public final static String KEY_CLUSTERED_VERTICES = "Clustered Vertices"; /** * Key used only with clusters. Under this key vertices have the cluster * they belong to. * @see #clusterGraph() */ public final static String KEY_CLUSTER = "Cluster"; /** * Key used only with clusters. Under this key a cluster has a boolean value * indicating that this vertice is a cluster (clusters are * VertexView-instances like every other cell). * @see #clusterGraph() */ public final static String KEY_IS_CLUSTER = "is Cluster"; /** * Key used only with clusters. Under this key every cluster has a position, * which represents the position of the cluster, right after the clustering * process. After the layout update process is finished, the move, resulting * of subtracting the position under {@link #KEY_POSITION} from the * position under this value, will be performed to all vertices in the * cluster. By holding the initial position here clustering becomes * possible. * * @see #clusterGraph() * @see #declusterGraph() */ public final static String KEY_CLUSTER_INIT_POSITION = "initial Position of the Cluster"; /** * List of all nodes in the graph. */ private ArrayList cellList; /** * List of all nodes the algorithm should be done for. */ private ArrayList applyCellList; /** * List of all edges in the graph. This is only needed for the optimization * Algorithm. */ private ArrayList edgeList; /** * needed for comperation with other double values if they are 0.0. */ private double equalsNull = 0.00000000000000001; /** * starting value for the temperatures of the cells */ private double initTemperature; /** * if the temperature of all cells or the average of the temperatures of * all cells is below this value, the algorithm stops */ private double minTemperature; /** * temperature will never be over this value */ private double maxTemperature; /** * the length of the Edges in Pixel, the algorithm tries to keep */ private double prefEdgeLength; /** * the strength of the gravitation force, directed to the barycenter of the * graph, added to all cells. */ private double gravitation; /** * length of a force vector with a random direction, added to all cells. */ private double randomImpulseRange; /** * opening angle in radiant, that detects oscillations */ private double alphaOsc; /** * opening angle in radiant, that detects rotations */ private double alphaRot; /** * penalty value for a detected oscillation */ private double sigmaOsc; /** * penalty value for a detected rotation */ private double sigmaRot; /** * number of rounds until the algorithm will break. This value is * precalculated to a aproximativ value of 4 times the count of Cells in * {@link #applyCellList} */ private int maxRounds; /** * counts the rounds */ private int countRounds; /** * If the pathlength between an inserted cell and an allready layouted cell * is below this value, the allready layouted cell will be layouted again. */ private int recursionDepth; /** * Describes the distance around a cell, that will be whatched for other * cells, intersecting this area. If another cell intersects, a force will * be added to the cell, that pushes it away. */ private double overlapDetectWidth; /** * Describes a distance the algorithm tries to keep, when he detects a * overlapping cell. */ private double overlapPrefDistance; /** * switches the feature to whatch for overlapping on/off */ private boolean avoidOverlapping; /** * describes, what method will be taken, when cells are inserted. Posible * values are * {@link GEMLayoutAlgorithm#KEY_LAYOUT_UPDATE_METHOD_NEIGHBORS_ONLY * KEY_LAYOUT_UPDATE_METHOD_NEIGHBORS_ONLY} and * {@link GEMLayoutAlgorithm#KEY_LAYOUT_UPDATE_METHOD_PERIMETERS * KEY_LAYOUT_UPDATE_METHOD_PERIMETERS}. */ private String layoutUpdateMethod; /** * condition for method isFrozen(). decides whether the method returns true * when the average of all temperatures or all temperatures are below * {@link #minTemperature}. */ private boolean shouldEndPerAverage; /** * condition for the method calculate(). decides whether the algorithm * is computed for the cellViews every time in the same sequence * or if the cellViews are computed every time in a random sequence. */ private boolean shouldComputePermutation; /** * Switches the skill of the algorithm to perform the layout update process */ private boolean isActive = true; /** * Checks if the algorithm is currently running. If this is the case, no * GraphModelEvent will be computed and no new run can be initiated. */ private boolean isRunning = false; /** * a reference to the instance of jgraph */ private JGraph jgraph; /** * the configuration of this algorithm */ protected Properties config; /* * shows the progress in performing the algorithm on the graphcells */ private ProgressDialog dlgProgress = new ProgressDialog((Frame) null, "Progress:", false); /* * to determine a somehow correct percentage value for the progress dialog */ private int[] phaseLength = new int[] { 2, 80, 100};//phase i goes up to /* * to detemine a correct labeled progress dialog */ private String[] phaseName = new String[] { "initialization", "performing calculation", "setting new Vertice Values"}; /** * to identify the different phases of the algorithm for the progress dialog */ private final static int PHASE_INITIALISATION = 0; /** * to identify the different phases of the algorithm for the progress dialog */ private final static int PHASE_CALCULATION = 1; /** * to identify the different phases of the algorithm for the progress dialog */ private final static int PHASE_END = 2; /** * to identify for the method {@link #loadRuntimeValues(int)}, that the * algorithm wants to perform a new run */ protected final static int VALUES_PUR = 0; /** * to identify for the method {@link #loadRuntimeValues(int)}, that the * algorithm wants to perform a layout update */ protected final static int VALUES_INC = 1; /** * algorithm used for optimizing the result of this algorithm */ private AnnealingLayoutAlgorithm optimizationAlgorithm; /** * switches the usage of the optimizing algorithm */ private boolean useOptimizeAlgorithm; /** * configuration of the optimizing algorithm */ private Properties optimizationAlgorithmConfig; /** * Switches clustering for the layout update process on/off */ private boolean isClusteringEnabled; /** * The initial temperature for clusters. It is recommended, that this value * is lower than {@link #initTemperature} to get a good looking layout */ private double clusterInitTemperature; /** * Scales forces, that are effecting clusters. It is recommendet to take * a value between 1.0 and 0.0. This garanties, that clusters move slower * than other cells. That rises the chance of getting a good looking layout * after the calculation. */ private double clusterForceScalingFactor; /** * Effects, how many clusters are created, when the layout update process * starts. This affects the initial number of clusters, which is the number * of cells available minus the number of cells to layout. The result of * that term is divided by this factor, to get the maximum number of * clusters. After this calculation, the clustering algorithm tries to * minimize the number of clusters, so there might be less clusters than * the maximum number. */ private double clusteringFactor; /** * The initial size for the layout update method perimeter. This describes * a radius around an inserted cell. Every other inserted cell in this * radius increases the radius by {@link #perimeterSizeInc}. After finishing * increasing the radius, every cell, from the cells, that are already * layouted, in the radius is added to the list of cells, that'll gain a * new position during the layout update process. This should bring up * the behaviour, that the previous layouted cells make space for the layout * of the inserted cells. */ private double perimeterInitSize; /** * Inserted cells whithin a radius of {@link #perimeterInitSize} around * a inserted cell are counted. After counting the inserted cells around * a inserted cell, the initial radius is increased by this increase value * times the number of inserted cells around the inserted cell. Every * previous layouted cell in the resulting radius around the inserted cell * is going to be layouted again. */ private double perimeterSizeInc; private boolean isDebugging = false; /******************************************************************************/ /** * Constructs a new GEM Layout Algorithm. */ public GEMLayoutAlgorithm(AnnealingLayoutAlgorithm optimizer){ cellList = new ArrayList(); applyCellList = new ArrayList(); edgeList = new ArrayList(); optimizationAlgorithm = optimizer; } /******************************************************************************/ /** * Starts the Calculation of a new layout with the GEM-Algorithm * @param graph View of Graphnodes * @param configuration contains the initial values for the Algorithm * @see #initialize() * @see #calculate() */ public void perform(JGraph graph, boolean applyToAll, Properties configuration) { isRunning = true; jgraph = graph; config = configuration; jgraph.getModel().addGraphModelListener(this); cellList = new ArrayList(); applyCellList = new ArrayList(); //extracting the nodes from jgraph, the algorithm should be performed on getNodes(jgraph,applyToAll); loadRuntimeValues(VALUES_PUR); long starttime = System.currentTimeMillis(); //ALGORITHM START boolean isCanceled = initialize();//initializes algorithm; sets the startvalues in cells if( !isCanceled ) isCanceled = calculate();//performs the algorithm on the cells //ALGORITHM END if( !isCanceled && useOptimizeAlgorithm ) isCanceled = optimizationAlgorithm.performOptimization(applyCellList,cellList,edgeList,optimizationAlgorithmConfig,dlgProgress); if( !isCanceled ) correctCoordinates(); //sets the calculated data into cellView's bounds if not canceled if( !isCanceled ) isCanceled = setNewCoordinates(jgraph); //removes the temporary data, stored by the algorithm, from the nodes removeTemporaryLayoutDataFromCells(); dlgProgress.setMessage("Algorithm finished"); //prepares for next calculation (maybe useless, because instance of //this algorithm might be thrown away) dlgProgress.setVisible(false); isRunning = false; } /******************************************************************************/ /** * Loads the actual desired values from the {@link #config configuration} to * the fields, where they are used later. * * @param valueID {@link #VALUES_PUR} for a normal run or {@link #VALUES_INC} * for a layout update process. */ protected void loadRuntimeValues(int valueID){ maxRounds = applyCellList.size() * 4;//estimated value; reached rarely countRounds = 0;//start value; counts the rounds in calculate() isActive = isTrue((String) config.get(GEMLayoutController.KEY_LAYOUT_UPDATE_ENABLED)); recursionDepth = Integer.parseInt((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_DEPTH)); layoutUpdateMethod = (String) config.get(GEMLayoutController.KEY_LAYOUT_UPDATE_METHOD); if( valueID == VALUES_PUR ){ initTemperature = Double.parseDouble((String)config.get( GEMLayoutController.KEY_INIT_TEMPERATURE)); minTemperature = Double.parseDouble((String)config.get( GEMLayoutController.KEY_MIN_TEMPERATURE)); maxTemperature = Double.parseDouble((String)config.get( GEMLayoutController.KEY_MAX_TEMPERATURE)); prefEdgeLength = Double.parseDouble((String)config.get( GEMLayoutController.KEY_PREF_EDGE_LENGTH)); gravitation = Double.parseDouble((String)config.get( GEMLayoutController.KEY_GRAVITATION)); randomImpulseRange = Double.parseDouble((String)config.get( GEMLayoutController.KEY_RANDOM_IMPULSE_RANGE)); overlapDetectWidth = Double.parseDouble((String)config.get( GEMLayoutController.KEY_OVERLAPPING_DETECTION_WIDTH)); overlapPrefDistance = Double.parseDouble((String)config.get( GEMLayoutController.KEY_OVERLAPPING_PREF_DISTANCE)); shouldEndPerAverage = isTrue((String)config.get( GEMLayoutController.KEY_COMPUTE_PERMUTATION)); shouldComputePermutation = isTrue((String)config.get( GEMLayoutController.KEY_END_CONDITION_AVERAGE)); avoidOverlapping = isTrue((String)config.get( GEMLayoutController.KEY_AVOID_OVERLAPPING)); alphaOsc = Double.parseDouble((String)config.get( GEMLayoutController.KEY_ALPHA_OSC)); alphaRot = Double.parseDouble((String)config.get( GEMLayoutController.KEY_ALPHA_ROT)); sigmaOsc = Double.parseDouble((String)config.get( GEMLayoutController.KEY_SIGMA_OSC)); sigmaRot = Double.parseDouble((String)config.get( //gets 1/x GEMLayoutController.KEY_SIGMA_ROT)); useOptimizeAlgorithm = isTrue((String)config.get( GEMLayoutController.KEY_OPTIMIZE_ALGORITHM_ENABLED)); optimizationAlgorithmConfig = (Properties) config.get( GEMLayoutController.KEY_OPTIMIZE_ALGORITHM_CONFIG); } else if( valueID == VALUES_INC ){ initTemperature = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_INIT_TEMPERATURE)); minTemperature = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_MIN_TEMPERATURE)); maxTemperature = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_MAX_TEMPERATURE)); prefEdgeLength = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_PREF_EDGE_LENGTH)); gravitation = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_GRAVITATION)); randomImpulseRange = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_RANDOM_IMPULSE_RANGE)); overlapDetectWidth = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_OVERLAPPING_DETECTION_WIDTH)); overlapPrefDistance = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_OVERLAPPING_PREF_DISTANCE)); shouldEndPerAverage = isTrue((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_COMPUTE_PERMUTATION)); shouldComputePermutation = isTrue((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_END_CONDITION_AVERAGE)); avoidOverlapping = isTrue((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_AVOID_OVERLAPPING)); alphaOsc = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_ALPHA_OSC)); alphaRot = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_ALPHA_ROT)); sigmaOsc = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_SIGMA_OSC)); sigmaRot = Double.parseDouble((String)config.get( //gets 1/x GEMLayoutController.KEY_LAYOUT_UPDATE_SIGMA_ROT)); useOptimizeAlgorithm = isTrue((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_OPTIMIZE_ALGORITHM_ENABLED)); optimizationAlgorithmConfig = (Properties) config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_OPTIMIZE_ALGORITHM_CONFIG); perimeterInitSize = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_METHOD_PERIMETER_INIT_SIZE)); perimeterSizeInc = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_METHOD_PERIMETER_SIZE_INC)); isClusteringEnabled = isTrue((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_CLUSTERING_ENABLED)); clusterInitTemperature = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_CLUSTERING_INIT_TEMPERATURE)); clusterForceScalingFactor = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_CLUSTERING_FORCE_SCALING_FACTOR)); clusteringFactor = Double.parseDouble((String)config.get( GEMLayoutController.KEY_LAYOUT_UPDATE_CLUSTERING_FACTOR)); } //with that line sigmaRot will be 1/(x*cellCount) with x is configurable sigmaRot *= 1.0 / (double) (applyCellList.size() == 0 ? 1 : applyCellList.size()); } /******************************************************************************/ /** * Helping method. Transforms a String, containing only the characters "true" or * "false", regardless if upper or lower case, into a boolean value. * * @param boolValue String containing a boolean value * @return boolean value represented by the given string */ protected boolean isTrue(String boolValue){ if( boolValue != null ){ if( "TRUE".equals(boolValue.toUpperCase()) ){ return true; } else if( "FALSE".equals(boolValue.toUpperCase()) ){ return false; } } return false; } /******************************************************************************/ /** * Extracts the Cells from JGraph and fills {@link #applyCellList}, * {@link #cellList} and {@link #edgeList}. If applyToAll is * false only in jgraph selected cells are added to * {@link #applyCellList} else all cells are added. * * @param jgraph actual instance of jgraph * @param applyToAll determines, if this algorithm should be performed on all * cells or only on the selected. */ private void getNodes(JGraph jgraph, boolean applyToAll){ Object[] cells = jgraph.getRoots(); Object[] selectedCells = jgraph.getSelectionCells(); CellView[] view = jgraph.getGraphLayoutCache().getMapping(cells,false); CellView[] selectedView = jgraph.getGraphLayoutCache().getMapping( selectedCells,false); for (int i = 0; i < view.length; i++) if (view[i] instanceof VertexView){ cellList.add(view[i]); if( applyToAll ) applyCellList.add(view[i]); } else if( view[i] instanceof EdgeView ){ edgeList.add(view[i]); } if( !applyToAll ) for( int i = 0; i < selectedView.length; i++ ) if( selectedView[i] instanceof VertexView ) applyCellList.add(selectedView[i]); } /******************************************************************************/ /** * Sets the initial Values, gained from the {@link #config configuration} * into the Cells. * * @return Because the progress dialog is allready visible during the * initialisation, true is returned when cancel is pressed * on it. */ private boolean initialize(){ dlgProgress.setVisible(true); dlgProgress.setMessage(phaseName[PHASE_INITIALISATION]); int length = cellList.size(); for( int i = 0; i < length; i++ ){ CellView view = (CellView) cellList.get(i); initializeVertice(view); if( updateProgressDialog(PHASE_INITIALISATION, i, length) ) return true; // canceled } for( int i = 0; i < applyCellList.size(); i++ ) computeLastImpulse( (CellView) applyCellList.get(i) ); return false; //not canceled } /******************************************************************************/ /** * Sets the initial values for one Cell. * * @param view Cell, the initial values should be set for. */ private void initializeVertice(CellView view){ Map attributes = view.getAttributes(); if( attributes == null ) attributes = new Hashtable(); attributes.put(KEY_CAPTION,KEY_CAPTION); initPosition(view); if( isCluster(view) ){ attributes.put(KEY_TEMPERATURE, new Double(clusterInitTemperature)); } else attributes.put(KEY_TEMPERATURE, new Double(initTemperature)); attributes.put(KEY_SKEWGAUGE, new Double(0.0)); attributes.put(KEY_CURRENT_IMPULSE,new Point2D.Double()); attributes.put(KEY_LAST_IMPULSE ,new Point2D.Double()); } /******************************************************************************/ /** * Runs the algorithm. First a running sequence is initialised. If * {@link #shouldComputePermuation} is true then a new * permutation is computed for every round, else a single determined sequence is * established. After this for every Cell a current impulse is calculated, * position and temperature is updated. This is done, until the graph is frozen, * a maximum on rounds is reached or cancel on the progress dialog is pressed. * * @return true when cancel on the progress dialog is * pressed * @see #computeCurrentImpulse(CellView) * @see #createPermutation(int) * @see #isFrozen() * @see #updatePosAndTemp(CellView) */ private boolean calculate(){ int length = applyCellList.size(); int[] sequence = new int[length]; boolean isCanceled = false; dlgProgress.setMessage(phaseName[PHASE_CALCULATION]); //case no permutation is desired, the series is computed one time only if( !shouldComputePermutation ) //else is in the loop below for( int i = 0; i < length; i++ ) sequence[i] = i; while( !isFrozen() && countRounds <= maxRounds && (!isCanceled) ){ //case permutation is desired, it's calculated every round if( shouldComputePermutation ) sequence = createPermutation(length); //loop over all nodes (order is in sequence) for( int i = 0; i < sequence.length; i++ ){ CellView view = (CellView) applyCellList.get(sequence[i]); computeCurrentImpulse(view); //computes direction of impulse updatePosAndTemp(view); //computes new position and temperature if( updateProgressDialog(PHASE_CALCULATION, (countRounds*sequence.length)+i, maxRounds*sequence.length ) ) return true; } countRounds++; } return false; } /******************************************************************************/ /** * Helps updating the progress dialog. * * @param phase Identifies the phase, the algorithmis doing. * @param round current round, the algorithm is performing * @param maxRound maximum number of rounds, the algorithm could perform */ private boolean updateProgressDialog(int phase, int round, int maxRound){ int lowValue = 0; if( phase != 0 ) lowValue = phaseLength[phase-1]; int maxValue = phaseLength[phase]; int width = maxValue - lowValue; int value = lowValue+(int)(width*((double)round/(double)maxRound)); dlgProgress.setValue(value); return dlgProgress.isCanceled(); } /******************************************************************************/ /** * Calculates the current impulse for the given cell. * * @param view Cell, the current impulse should be calculated * @see #computeImpulse(CellView) */ private void computeCurrentImpulse(CellView view){ //gets the impulse for view Point2D.Double impulse = computeImpulse(view); //set result into node view.getAttributes().put(KEY_CURRENT_IMPULSE,impulse); } /******************************************************************************/ /** * Calculates the last impulse for the given cell. This is only nesessary while * initializing the cells. * * @param view Cell, the last impulse should be calculated * @see #computeImpulse(CellView) */ private void computeLastImpulse(CellView view){ //gets the impulse for view Point2D.Double impulse = computeImpulse(view); //set result into node view.getAttributes().put(KEY_LAST_IMPULSE,impulse); } /******************************************************************************/ /** * Computes an Impulse representing a Force affecting the position of the given * Cell. This impulse consists of a attracting force, pulling the cell to the * barycenter of all cells, a pulse with user defined length and random * direction, a force repulsing cells from each other, a force attracting * connected cells together, and, as a additional feature, a force, repulsing * the current cell from overlapping cells. * * @param view Cell, the impulse should be computed * @return impulse, transformed in a Point2D.Double-Instance. */ private Point2D.Double computeImpulse(CellView view){ Point2D.Double impulse = new Point2D.Double(); Point2D.Double pos = getPosition(view); boolean isCellACluster = isCluster(view); //the more edges a cell have, the heavier the cell is double massIndex = getNodeWeight(view); //gets the barycenter of all cells Point2D.Double barycenter = computeBarycenter(cellList); //attracting force from the barycenter to every cell Point2D.Double gravitationForce = new Point2D.Double( (barycenter.getX() - pos.getX()) * gravitation * massIndex, (barycenter.getY() - pos.getY()) * gravitation * massIndex); //random glitch is added to force Point2D.Double randomImpulse = getRandomVector(randomImpulseRange); //repulsive Forces //from all nodes ArrayList repulsiveForce = new ArrayList(); for( int i = 0 ; i < cellList.size(); i++ ) if( cellList.get(i) != view ){//all cells except the actual view // CellView uView = (CellView) cellList.get(i); // if( !isCluster(uView)){ Point2D.Double uPos = getPosition(i,cellList); double deltaX = (double)(pos.getX() - uPos.getX()); double deltaY = (double)(pos.getY() - uPos.getY()); /* double sgnX = MathExtensions.sgn(deltaX); double sgnY = MathExtensions.sgn(deltaY); if( isCellACluster && isCluster(uView) ){ deltaX -= uView.getBounds().getWidth() / 2.0; deltaY -= uView.getBounds().getHeight()/ 2.0; } if( isCellACluster ){ deltaX -= view.getBounds().getWidth() / 2.0; deltaY -= view.getBounds().getHeight()/ 2.0; } if( sgnX != MathExtensions.sgn(deltaX) ){ deltaX *= sgnX; } if( sgnY != MathExtensions.sgn(deltaY) ){ deltaY *= sgnY; } */ double absDelta = MathExtensions.abs(deltaX,deltaY); if( absDelta > equalsNull ){ repulsiveForce.add(new Point2D.Double( deltaX * ((prefEdgeLength * prefEdgeLength) / (absDelta * absDelta)), deltaY * ((prefEdgeLength * prefEdgeLength) / (absDelta * absDelta)))); } // } } //attractive Forces: //from all nodes that have an edge with view ArrayList relatives = getRelativesFrom(cellList,view); ArrayList attractiveForce = new ArrayList(relatives.size()); for( int i = 0; i < relatives.size(); i++ ){ // CellView child = (CellView) relatives.get(i); Point2D.Double cPos = getPosition(i,relatives); double deltaX = (double)(pos.getX() - cPos.getX()); double deltaY = (double)(pos.getY() - cPos.getY()); /* double sgnX = MathExtensions.sgn(deltaX); double sgnY = MathExtensions.sgn(deltaY); if( isCellACluster && isCluster( child )){ deltaX -= child.getBounds().getWidth() / 2.0; deltaY -= child.getBounds().getHeight()/ 2.0; } if( isCellACluster ){ deltaX -= view.getBounds().getWidth() / 2.0; deltaY -= view.getBounds().getHeight()/ 2.0; } if( sgnX != MathExtensions.sgn(deltaX) ) deltaX *= sgnX; if( sgnY != MathExtensions.sgn(deltaY) ) deltaY *= sgnY; */ double absDelta = MathExtensions.abs(deltaX,deltaY); attractiveForce.add(new Point2D.Double( deltaX * (( absDelta * absDelta ) / ( prefEdgeLength * prefEdgeLength * massIndex )), deltaY * (( absDelta * absDelta ) / ( prefEdgeLength * prefEdgeLength * massIndex )))); } /* the next part is NOT part of the original algorithm */ /* it adds a force if the actual cell overlapps another cell */ ArrayList forcesByOverlapping = new ArrayList(); if( avoidOverlapping ){ Rectangle viewBounds = new Rectangle( (int)pos.x, (int)pos.y, (int)view.getBounds().getWidth(), (int)view.getBounds().getHeight()); Rectangle viewBorder = new Rectangle( (int)(viewBounds.getX()-overlapDetectWidth), (int)(viewBounds.getY()-overlapDetectWidth), (int)(viewBounds.getWidth()+(2.0*overlapDetectWidth)), (int)(viewBounds.getHeight()+(2.0*overlapDetectWidth))); for( int i = 0; i < cellList.size(); i++ ){ Point2D.Double uPos = getPosition(i,cellList); Rectangle uBounds = new Rectangle( (int)uPos.x, (int)uPos.y, (int)((CellView)cellList.get(i)).getBounds().getWidth(), (int)((CellView)cellList.get(i)).getBounds().getHeight()); if( view != cellList.get(i) && viewBorder.intersects(uBounds) ){ Dimension viewSize = viewBounds.getSize(); Dimension uSize = uBounds.getSize(); double minDistance = (Math.max(viewSize.getWidth(),viewSize.getHeight())/2.0)+ (Math.max(uSize.getWidth(),uSize.getHeight())/2.0)+overlapPrefDistance; double deltaX = (double)(pos.x - uPos.x); double deltaY = (double)(pos.y - uPos.y); /* if( isCellACluster ){ deltaX -= view.getBounds().getWidth() / 2.0; deltaY -= view.getBounds().getHeight()/ 2.0; } if( isCluster((CellView)cellList.get(i))){ deltaX -= ((CellView)cellList.get(i)).getBounds().getWidth() / 2.0; deltaY -= ((CellView)cellList.get(i)).getBounds().getHeight()/ 2.0; }*/ if( deltaX < equalsNull && deltaX >= 0.0 ){ deltaX = equalsNull; } else if( deltaX > -equalsNull && deltaX <= 0.0 ){ deltaX = -equalsNull; } if( deltaY < equalsNull && deltaY >= 0.0 ){ deltaY = equalsNull; } else if( deltaY > -equalsNull && deltaY <= 0.0 ){ deltaY = -equalsNull; } double absDelta = MathExtensions.abs(deltaX,deltaY); Point2D.Double force = new Point2D.Double( deltaX*(minDistance*minDistance)/(absDelta*absDelta), deltaY*(minDistance*minDistance)/(absDelta*absDelta)); // System.out.println("Overlapping Nodes: ("+pos.x+"|"+pos.y+") and ("+uPos.x+"|"+uPos.y+") -> Distance = "+absDelta+" -> Force = "+force); forcesByOverlapping.add(force); } } } //for classes that extend this algorithm ArrayList additionalForce = getAdditionalForces((VertexView)view); //adding the forces impulse = add(impulse,gravitationForce); impulse = add(impulse,randomImpulse); for( int i = 0; i < repulsiveForce.size(); i++ ) impulse = add(impulse,(Point2D.Double) repulsiveForce.get(i) ); for( int i = 0; i < attractiveForce.size(); i++ ) impulse = sub(impulse,(Point2D.Double) attractiveForce.get(i) ); for( int i = 0; i < forcesByOverlapping.size(); i++ ) impulse = add(impulse,(Point2D.Double) forcesByOverlapping.get(i)); for( int i = 0; i < additionalForce.size(); i++ ) impulse = add(impulse,(Point2D.Double) additionalForce.get(i) ); return impulse; } /******************************************************************************/ /** * Updating the position of the given cell, by taking the direction of the * current impulse and the length of the temperature of the cell. After this * temperature will fall, when the last impulse and the current impulse are * part of a rotation or a oscillation, temperature of the cell will be * decreased. * * @param view Cell that should be updated */ private void updatePosAndTemp(CellView view){ Point2D.Double impulse = (Point2D.Double) view.getAttributes().get(KEY_CURRENT_IMPULSE); Point2D.Double lastImpulse = (Point2D.Double) view.getAttributes().get(KEY_LAST_IMPULSE); Point2D.Double position = getPosition(view); double localTemperature = ((Double) view.getAttributes().get(KEY_TEMPERATURE)).doubleValue(); double skewGauge = ((Double) view.getAttributes().get(KEY_SKEWGAUGE)).doubleValue(); double absImpulse = MathExtensions.abs(impulse); double absLastImpulse = MathExtensions.abs(lastImpulse); if( absImpulse > equalsNull ){ //if impulse != 0 //scaling with temperature if( isCluster(view) ){ impulse.setLocation( impulse.getX() * localTemperature * clusterForceScalingFactor / absImpulse, impulse.getY() * localTemperature * clusterForceScalingFactor / absImpulse); } else { impulse.setLocation( impulse.getX() * localTemperature / absImpulse, impulse.getY() * localTemperature / absImpulse); } view.getAttributes().put(KEY_CURRENT_IMPULSE,impulse); position.setLocation(position.getX()+impulse.getX(), position.getY()+impulse.getY()); view.getAttributes().put(KEY_POSITION,position); /* if( isDebugging ){ check(impulse,"impulse12"); check(position,"position12"); }*/ } if( absLastImpulse > equalsNull ){ //beta = angle between new and last impulse double beta = MathExtensions.angleBetween(impulse,lastImpulse); double sinBeta = Math.sin(beta); double cosBeta = Math.cos(beta); //detection for rotations if( Math.abs(sinBeta) >= Math.sin((Math.PI/2.0)+(alphaRot/2.0)) ) skewGauge += sigmaRot * MathExtensions.sgn(sinBeta); //detection for oscillation if( cosBeta < Math.cos(Math.PI+(alphaOsc/2.0)) ) localTemperature *= sigmaOsc * Math.abs(cosBeta); localTemperature *= 1.0 - Math.abs(skewGauge); localTemperature = Math.min(localTemperature,maxTemperature); } //applying changes view.getAttributes().put(KEY_TEMPERATURE,new Double(localTemperature)); view.getAttributes().put(KEY_POSITION ,position); view.getAttributes().put(KEY_SKEWGAUGE ,new Double(skewGauge)); view.getAttributes().put(KEY_LAST_IMPULSE,new Point2D.Double( impulse.getX(), impulse.getY())); /* if( isDebugging ) checkCellList();*/ } /******************************************************************************/ /** * Adding two forces. * @param v1 Force that should be added with v2 * @param v2 Force that should be added with v1 * @return Sum of both forces. */ private Point2D.Double add(Point2D.Double v1, Point2D.Double v2){ return new Point2D.Double(v1.getX()+v2.getX(),v1.getY()+v2.getY()); } /******************************************************************************/ /** * Subtracing two forces. * @param v1 Force, v2 should be subtracted from * @param v2 Force, that should be subtracted from v1. */ private Point2D.Double sub(Point2D.Double v1, Point2D.Double v2){ return new Point2D.Double(v1.getX()-v2.getX(),v1.getY()-v2.getY()); } /******************************************************************************/ /** * Returns all Cells, that have a direct connection with the given cell and are * a member of the given list. * * @param list List of some cells, that should contain some relatives from view * @param view Cell, the relatives are requested from * @return List of all relatives that are in list. * @see #getRelatives(CellView) */ private ArrayList getRelativesFrom(ArrayList list, CellView view){ ArrayList relatives = getRelatives(view); ArrayList result = new ArrayList(); for( int i = 0; i < relatives.size(); i++ ) if( list.contains(relatives.get(i)) ) result.add(relatives.get(i)); return result; } /******************************************************************************/ /** * Returns a list of all cells, that have a direct connection with the given * cell via a edge. At the end of this method, the result is stored in the given * cell, so it will be available the next time, the method runs. This temporary * stored data will stay there, until the algorithm finishes * (successfull or not). * * @param view Cell, the relatives requested from. * @return List of all cells, that have a direct connection with a edge to * the given cell. */ private ArrayList getRelatives(CellView view){ if( !(view instanceof VertexView) ) { new Exception("getRelatives 1").printStackTrace(); return null; } if( view.getAttributes().containsKey(KEY_RELATIVES) ) return (ArrayList) view.getAttributes().get(KEY_RELATIVES); ArrayList relatives = new ArrayList(); //if view is a cluster, then all clustered cells are extracted and //getRelatives is called for every cell again. the resulting relatives //are checked, if they are in the same cluster or another cluster. //if the last condition is the case, the cluster is added, else the //vertex is added to the list of relatives, iff he isn't already in the //list if( isCluster(view) ){ ArrayList clusteredVertices = (ArrayList) view.getAttributes().get(KEY_CLUSTERED_VERTICES); for( int i = 0; i < clusteredVertices.size(); i++ ){ ArrayList vertexRelatives = getRelatives( (CellView)clusteredVertices.get(i)); for( int j = 0; j < vertexRelatives.size(); j++ ){ CellView relative = (CellView) vertexRelatives.get(j); if( !clusteredVertices.contains(relative) ){ /* if( relative.getAttributes().containsKey(KEY_CLUSTER) ){ relative = (CellView) relative.getAttributes().get(KEY_CLUSTER); }*/ if( !relatives.contains(relative)) relatives.add(relative); } } } } else { //runs only for vertices. finds all ports of the vertex. every //edge in every port is checked on their source and target. //the one, that isn't the vertex, we are searching the relatives of, //is added to the list of relatives. ArrayList portsCells = new ArrayList(); VertexView vertexView = (VertexView)view; GraphModel model = vertexView.getModel(); CellMapper mapper = vertexView.getMapper() ; Object vertexCell = vertexView.getCell() ; for (int i = 0; i < model.getChildCount(vertexCell); i++){ Object portCell = model.getChild(vertexCell, i); portsCells.add(portCell); } for( int i = 0; i < portsCells.size() ; i++ ){ Object portCell = portsCells.get(i); Iterator edges = model.edges(portCell); while (edges.hasNext() ){ Object edge = edges.next() ; Object nextPort = null; if( model.getSource(edge) != portCell ){ nextPort = model.getSource(edge); } else { nextPort = model.getTarget(edge); } CellView nextVertex = mapper.getMapping( model.getParent(nextPort), true); relatives.add(nextVertex); } } } view.getAttributes().put(KEY_RELATIVES,relatives); return relatives; } /******************************************************************************/ /** * This is a rating method for the cells. It is used during * {@link #computeImpulse(CellView)} scale some forces. * * @param view Cell, the weight is of interest. */ //TODO: method doesn't work right for clusters private double getNodeWeight(CellView view){ if( view.getAttributes().containsKey(KEY_MASSINDEX) ) return ((Double)view.getAttributes().get(KEY_MASSINDEX)). doubleValue(); int childCount = getRelatives(view).size(); double massIndex = (double)(childCount + 1) / 2.0; view.getAttributes().put(KEY_MASSINDEX,new Double(massIndex)); return massIndex; } /******************************************************************************/ /** * Applies the changes to the Cells. This means, that all temporary stored * positions are applied to all cells in {@link #applyCellList} */ private boolean setNewCoordinates(JGraph jgraph){ Map viewMap = new Hashtable(); // dlgProgress.setMessage(phaseName[PHASE_END]); for( int i = 0; i < cellList.size(); i++ ){ Point2D.Double pos = getPosition(i,cellList); Rectangle r = ((CellView)cellList.get(i)).getBounds(); r.x = (int) (pos.getX() - ((double)r.width /2.0)); r.y = (int) (pos.getY() - ((double)r.height/2.0)); Object cell = ((CellView) cellList.get(i)).getCell(); Map attributes = GraphConstants.createMap(); GraphConstants.setBounds(attributes, r); viewMap.put(cell, attributes); // if( updateProgressDialog(PHASE_END,i,applyCellList.size()) ) // return true; } jgraph.getGraphLayoutCache().edit(viewMap,null,null,null); return false; } /******************************************************************************/ /** * Clears the temporary data from the cells in {@link #cellList} (all cells). */ private void removeTemporaryLayoutDataFromCells(){ for( int i = 0; i < cellList.size(); i++ ) ((CellView)cellList.get(i)).getAttributes().clear(); } /******************************************************************************/ /** * Checks, if the algorithm could break it's calculation earlier, than * performing until {@link #countRounds} is {@link #maxRounds}. This depends on * {@link #shouldEndPerAverage}. * * @param true either when the average the temperature of * all cells is below {@link #minTemperature} or the temperature itself of all * cells is below. */ private boolean isFrozen(){ double sumOfTemp = 0.0; //sum of temperatures to get the average value double globalTemp = 0.0; //average value of all temperatures boolean isFrozen = true;//true while all temperatures <= minTemperature for( int i = 0; i < applyCellList.size(); i++ ){ double temperature = getTemperature(i,applyCellList); sumOfTemp += temperature; isFrozen = isFrozen && (temperature <= minTemperature); if( !isFrozen && !shouldEndPerAverage )//speeds up a little break; } if( shouldEndPerAverage ){ globalTemp = sumOfTemp / (double)applyCellList.size(); return globalTemp < minTemperature; } else return isFrozen; } /******************************************************************************/ /** * Erzeugt eine Permutation der Zahlen von 0 bis length * * @param length Count and highest value of the generated sequence. * @return sequence of numbers, contains every number a single time. The * sequence consists of numbers between 0 and length. */ private int[] createPermutation(int length){ int[] permutation = new int[length]; for( int i = 0; i < permutation.length; i++ ){ int newValue = (int)(Math.random()*(double)length); for( int j = 0; j < i; j++ ) if( newValue == permutation[j] ){ newValue = (int)(Math.random()*(double)length); j = -1; // wird auf 0 zur�ckgesetzt } permutation[i] = newValue; } return permutation; } /******************************************************************************/ /** * Creates a random Vector, with a given length and a random direction. * * @param length Length of the Vector created by this method * @return Vector represented by a Point2D.Double */ private Point2D.Double getRandomVector(double length){ double alpha = Math.random()*Math.PI*2; // double length = Math.random()*maxLength; return new Point2D.Double(length*Math.cos(alpha), length*Math.sin(alpha)); } /******************************************************************************/ /** * Calculates the barycenter of a graph, given by a list. This calculation is * done by summing the coordinates and dividing them with the number of * coordinates. * * @param list List of CellView's * @return Position of the barycenter */ private Point2D.Double computeBarycenter(ArrayList list){ double sumX = 0.0; double sumY = 0.0; for( int i = 0; i < list.size(); i++ ){ CellView view = (CellView) list.get(i); initPosition(view); Point2D.Double pos = getPosition(view); sumX += pos.x; sumY += pos.y; } return new Point2D.Double(sumX/((double)list.size()), sumY/((double)list.size())); } /******************************************************************************/ /** * Initialilzes the position of a CellView to the center point of the bounds * of the cell. This initialization is only be done, when the cell isn't * initialised before. * * @param view Cell, the position should be initialized. */ private void initPosition(CellView view){ if( !view.getAttributes().containsKey(KEY_POSITION) ) view.getAttributes().put(KEY_POSITION,new Point2D.Double( view.getBounds().getCenterX(), view.getBounds().getCenterY())); } /******************************************************************************/ /** * Moves the graph to the upper left corner of the drawing space. This is done, * after a successfull run of the algorithm, to correct it's output. */ private void correctCoordinates(){ Rectangle boundingBox = getBoundingBox(); if( boundingBox != null ){ for( int i = 0; i < cellList.size(); i++ ){ CellView view = (CellView) cellList.get(i); Point2D.Double pos = getPosition(view); Point2D.Double newPos = new Point2D.Double( pos.x-boundingBox.getX(), pos.y-boundingBox.getY()); view.getAttributes().put(KEY_POSITION,newPos); } } } /******************************************************************************/ /** * Computes the bounding box of the whole graph. The result is a Rectangle, * parallel to the X- and Y-axises of the drawing system, closing about the * whole graph. * @return Rectangle, that contains the whole graph. * @see #getBoundingBox(ArrayList) */ private Rectangle getBoundingBox(){ return getBoundingBox(cellList); } /******************************************************************************/ /** * Computes the bounding box of the graph in the given list of CellViews. * The result is a Rectangle, parallel to the X- and Y-axises of the drawing * system, closing about the graph in the given list. * * @param verticeList List containing the CellViews, the bounding box is of * interest. * @return Rectangle, that contains the whole graph, linked in the given list. */ private Rectangle getBoundingBox(ArrayList verticeList){ if( verticeList.size() > 0 ){ Point2D.Double vertexPos = getPosition(0,verticeList); Dimension vertexSize = ((CellView)verticeList.get(0)).getBounds().getSize(); double minX = vertexPos.getX(); double minY = vertexPos.getX(); double maxX = vertexPos.getX()+vertexSize.getWidth(); double maxY = vertexPos.getX()+vertexSize.getHeight(); for( int i = 1; i < verticeList.size(); i++ ){ vertexPos = getPosition(i,verticeList); vertexSize =((CellView)verticeList.get(i)).getBounds().getSize(); if( minX > vertexPos.getX() ) minX = vertexPos.getX(); if( minY > vertexPos.getY() ) minY = vertexPos.getY(); if( maxX < vertexPos.getX()+vertexSize.getWidth() ) maxX = vertexPos.getX()+vertexSize.getWidth(); if( maxY < vertexPos.getY()+vertexSize.getHeight() ) maxY = vertexPos.getY()+vertexSize.getHeight(); } Rectangle boundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX-minX), (int)(maxY-minY)); return boundingBox; } return null; } /******************************************************************************/ /** * Returns the Position of a Cell contained in {@link #applyCellList}. * * @param index Identifies the cell. This is the index of the cell in * the given list of CellViews * @param list List containing only CellViews * @see #getAttribute(int,String,ArrayList) */ private Point2D.Double getPosition(int index, ArrayList list){ return (Point2D.Double) getAttribute(index,KEY_POSITION,list); } /******************************************************************************/ /** * Returns the temperature of a cell contained in a given list. * * @param index Identifies the cell. This is the index of the cell in * a given list of CellViews * @param list List containing only CellViews * @see #getAttribute(int,String,ArrayList) */ private double getTemperature(int index, ArrayList list){ Double temperature = (Double) getAttribute(index,KEY_TEMPERATURE,list); return temperature.doubleValue(); } /******************************************************************************/ /** * Returns the Position of a Cell. * * @param cell The cell, that holds the position of interest. */ private Point2D.Double getPosition(CellView cell){ return (Point2D.Double) cell.getAttributes().get(KEY_POSITION); } /******************************************************************************/ /** * Returns an attribute from a cell contained in a given list of CellViews. * * @param index Identifies the cell. This is the index of the cell in * the given list of CellViews * @param key Identifies the Attribute, that should be retrieved. * @param list List containing only CellViews */ private Object getAttribute(int index, String key, ArrayList list){ CellView view = (CellView) list.get(index); return view.getAttributes().get(key); } /******************************************************************************/ /** * Arranges the initial cositions for the inserted cells. This is done, by * placing the inserted cells in the barycenter of their relatives. * (possible with errors ... might be fixed soon) */ private void arrangePlacement(CellView[] views){ for( int i = 0; i < cellList.size(); i++ ) initPosition((CellView)cellList.get(i)); if( views != null ){ if( views.length > 0 ){ ArrayList cellLevelList = new ArrayList(); for( int i = 0; i < views.length; i++ ){ if( views[i] instanceof VertexView ){ ArrayList relatives = getRelativesFrom(cellList, views[i]); if( relatives.size() > 0 ){ if( views[i].getAttributes() == null ) views[i].setAttributes(new Hashtable()); views[i].getAttributes().put(KEY_POSITION, computeBarycenter(relatives)); cellLevelList.add(views[i]); } } } for( int i = 0; i < cellLevelList.size(); i++ ) cellList.add(cellLevelList.get(i)); int childViewCount = 0; CellView[] possibleChildViews = new CellView[views.length-cellLevelList.size()]; for( int i = 0; i < views.length; i++ ) if( !cellLevelList.contains(views[i]) ) possibleChildViews[childViewCount++] = views[i]; arrangePlacement(possibleChildViews); } } } /******************************************************************************/ /** * Method for the process of layout update. Adds inserted cells to * {@link #applyCellList} and some of their neighbors. Adding of neighbors is * deceided by {@link #layoutUpdateMethod}. If a method is choosen with * perimeter, than the inserted cells are counted, that have a position whithin * the basic radius around inserted cells. then a new radius is calculated by * multiplying the increasial radius with the number of inserted cells found and * adding it to the basic radius. Then all cells, previously layouted whithin * this radius are also added to {@link #applyCellList}. After this, cells * within a given pathlength smaller than {@link #recursionDepth} are added * to {@link #applyCellList} too. * * @param vertexList List of all inserted Vertices. */ public void addApplyableVertices(VertexView[] vertexList){ for( int i = 0; i < vertexList.length; i++ ){ if( !applyCellList.contains(vertexList[i]) ) applyCellList.add(vertexList[i]); if( !cellList.contains(vertexList[i]) ) cellList.add(vertexList[i]); } if( GEMLayoutController.KEY_LAYOUT_UPDATE_METHOD_PERIMETERS.equals(layoutUpdateMethod) ){ for( int i = 0; i < vertexList.length; i++ ){ double perimeterSize = perimeterInitSize; Point2D.Double pos = getPosition(vertexList[i]); for( int j = 0; j < vertexList.length; j++ ){ if( i != j ){ Point2D.Double oPos = getPosition(vertexList[j]); if( Math.abs(pos.distance(oPos)) < (perimeterInitSize / 2.0) ) perimeterSize += perimeterSizeInc; } } for( int j = 0; j < cellList.size(); j++ ){ Point2D.Double uPos = getPosition(j,cellList); if( Math.abs(pos.distance(uPos)) < (perimeterSize / 2.0) && !applyCellList.contains(cellList.get(j)) ) applyCellList.add(cellList.get(j)); } } vertexList = new VertexView[applyCellList.size()]; for( int i = 0; i < applyCellList.size(); i++ ) vertexList[i] = (VertexView) applyCellList.get(i); } if( recursionDepth > 0 ) addRelativesToList(vertexList,recursionDepth); } /******************************************************************************/ /** * Recursiv method for adding all relatives whithin a given pathlength away * from the given array of Vertices to {@link #applyCellList}. * * @param vertexList List of Vertices, which relatives might be added. * @param depth pathlength, the vertices adding to {@link #applyCellList} could * be away from the given array's vertices. */ private void addRelativesToList(VertexView[] vertexList, int depth){ if( vertexList == null ) return; if( vertexList.length == 0 ) return; if( depth == 0 ) return; for( int i = 0; i < vertexList.length; i++ ){ ArrayList relatives = getRelatives(vertexList[i]); VertexView[] relativeList = new VertexView[relatives.size()]; for( int j = 0; j < relatives.size(); j++ ){ if( !applyCellList.contains(relatives.get(j)) ) applyCellList.add(relatives.get(j)); if( !cellList.contains(relatives.get(j)) ) cellList.add(relatives.get(j)); relativeList[j] = (VertexView) relatives.get(j); } addRelativesToList(relativeList,depth-1); } } /******************************************************************************/ /** * Method for Classes that extend this Algorithm. Will be called when performing * {@link #computeImpulse(CellView)}. */ protected ArrayList getAdditionalForces(VertexView view){ return new ArrayList(); } /******************************************************************************/ /** * Will be called, when cells are inserted or removed. When cells are removed, * they are also removed from {@link #cellList}, {@link #applyCellList} and * {@link #edgeList}. If cells are inserted a new layout update process starts. */ public void graphChanged(GraphModelEvent e){ if( !isRunning && isActive ){ isRunning = true; GraphModelEvent.GraphModelChange change = e.getChange(); Object[] objRem = change.getRemoved(); Object[] objIns = change.getInserted(); if( objRem == null && objIns != null ){ // nodes inserted for( int i = 0; i < cellList.size(); i++ ) initPosition((CellView)cellList.get(i)); CellView[] viewInserted = jgraph.getGraphLayoutCache().getMapping(objIns,false); applyCellList = new ArrayList(); /*extracting vertices into []*/ int vertexViewCount = 0; for( int i = 0; i < viewInserted.length; i++ ) if( viewInserted[i] instanceof VertexView ) vertexViewCount++; VertexView[] vertexList = new VertexView[vertexViewCount]; vertexViewCount = 0; for( int i = 0; i < viewInserted.length; i++ ) if( viewInserted[i] instanceof VertexView ) vertexList[vertexViewCount++] = (VertexView) viewInserted[i]; /*extracting vertices into [] done*/ //stops inserting process, if no vertex was inserted if( vertexList.length == 0 ){ isRunning = false; return; } //initialising runtime values from config loadRuntimeValues(VALUES_INC); //the number of cells in applyCellList will probably change sigmaRot /= 1.0 / (double) applyCellList.size(); //positioning the new nodes in the barycenter of old relatives arrangePlacement(vertexList); //add new vertices and some relatives to applyCellList addApplyableVertices(vertexList); if( applyCellList.size() == 0 ){ isRunning = false; return; } // showCellList(applyCellList,Color.GREEN); if( isClusteringEnabled ){ clusterGraph(); } //the number of cells in applyCellList has changed probably sigmaRot *= 1.0 / (double) applyCellList.size(); maxRounds = applyCellList.size() * 4; // performing algorithm on all nodes in applyCellList initialize(); calculate(); // algorithm done if( isClusteringEnabled ) declusterGraph(); if( useOptimizeAlgorithm ) optimizationAlgorithm.perform(jgraph,false,optimizationAlgorithmConfig); //moves graph to the upper left corner correctCoordinates(); //taking changes setNewCoordinates(jgraph); //removing algorithms attributes from nodes removeTemporaryLayoutDataFromCells(); } else if( objRem != null && objIns == null ){ // nodes removed CellView[] viewRemoved = jgraph.getGraphLayoutCache().getMapping(objRem,false); for( int i = 0; i < viewRemoved.length; i++ ){ if( viewRemoved[i] instanceof VertexView && cellList.contains(viewRemoved[i]) ){ applyCellList.remove(viewRemoved[i]); cellList.remove(viewRemoved[i]); } } } isRunning = false; } } /******************************************************************************/ /******************** CLUSTERING METHODS **************************************/ /******************************************************************************/ /** * Clusters a graph. Cells, contained in {@link #cellList} and not contained * in {@link #applyCellList} are clustered by this short algorithm. The * algorithm first tries to identify how many cells it should cluster. This * is calculated by subtracting the size of {@link #applyCellList} from * the size of {@link #cellList} and dividing the result by the * {@link #clusteringFactor}. In the next step, the identified number of * clusters are created, and their position is initialised by random. Then * every clusterable cell is added to the cluster where the distance of the * vertex and the cluster is minimal. After adding a cell, the clusters position * is recalculated. Finishing this step, the algorithm tries to minimize the * number of clusters, by sorting the clustered vertices, if there is another * cluster, that distance is shorter than the distance to the cluster, the * vertice is actually in. This can happen, because by moving vertices into the * clusters, the position of the clusters are changed. The minimization runs * until no vertice can be moved anymore. empty clusters are removed and finaly * the clusters are added to {@link #applyCellList}, because they should move * while the upcoming next calculations. That move can later be retrieved by * subtracting the attributes {@link #KEY_POSITION} and * {@link #KEY_CLUSTER_INIT_POSITION}. * * @see #declusterGraph() */ protected void clusterGraph(){ //initialisation int maxClusters = (int)((double)(cellList.size() - applyCellList.size()) / clusteringFactor ); if( maxClusters == 0 ){ System.out.println("maxClusters = 0"); return; } if( cellList.size() <= 1 ){ System.out.println("cellList.size() <= 1"); return; } ArrayList clusterList = new ArrayList(); ArrayList cellsToCluster = new ArrayList(); //identifying all cells, that are clusterable for( int i = 0; i < cellList.size(); i++ ) if( !applyCellList.contains(cellList.get(i)) ) cellsToCluster.add(cellList.get(i)); //initialize clusters VertexView[] clusters = new VertexView[maxClusters]; CellMapper mapper = ((VertexView)cellList.get(0)).getMapper(); Rectangle boundingBox = getBoundingBox(); for( int i = 0; i < clusters.length; i++ ){ clusters[i] = new VertexView(null,jgraph,mapper); Map attributes = clusters[i].getAttributes(); attributes.put(KEY_IS_CLUSTER,"true"); attributes.put(KEY_POSITION,new Point2D.Double( Math.random()*boundingBox.width, Math.random()*boundingBox.height)); clusterList.add(clusters[i]); } //cluster all available cells for( int i = 0; i < cellsToCluster.size(); i++ ){ VertexView cell = (VertexView) cellsToCluster.get(i); Point2D.Double cellPos = getPosition(cell); int clusterID = 0; Point2D.Double clusterPos = getPosition((CellView)clusterList.get(0)); double minDistance = MathExtensions.getEuclideanDistance(cellPos,clusterPos); //search for nearest cluster for( int j = 1; j < clusterList.size(); j++ ){ clusterPos = getPosition(j,clusterList); double distance = MathExtensions.getEuclideanDistance(cellPos,clusterPos); if( minDistance > distance ){ minDistance = distance; clusterID = j; } } VertexView cluster = (VertexView) clusterList.get(clusterID); moveVerticeToCluster(cell,cluster); } //initialization done //sorting the clustered vertices. if a vertice is nearer to a clusters //barycenter then to it's own clusters barycenter the vertice is moved //to that cluster. The coordinates of both clusters are recalculated. //this is done, until nothing could be done better. boolean couldMakeItBetter = false; do { couldMakeItBetter = false; for( int i = 0; i < cellsToCluster.size(); i++ ){ VertexView cell = (VertexView) cellsToCluster.get(i); VertexView oldCluster = (VertexView) cell.getAttributes().get(KEY_CLUSTER); Point2D.Double cellPos = getPosition(cell); Point2D.Double clusterPos = getPosition(oldCluster); double distance = MathExtensions.getEuclideanDistance(cellPos,clusterPos); for( int j = 0; j < clusterList.size(); j++ ){ VertexView cluster = (VertexView) clusterList.get(j); if( cluster != oldCluster ){ clusterPos = getPosition(cluster); double newDistance = MathExtensions.getEuclideanDistance(cellPos,clusterPos); if( newDistance < distance ){ moveVerticeToCluster(cell,cluster); couldMakeItBetter = true; break; } } } } } while( couldMakeItBetter ); //empty clusters are removed for( int i = 0; i < clusterList.size(); i++ ){ if( !((VertexView)clusterList.get(i)).getAttributes().containsKey(KEY_CLUSTERED_VERTICES)){ clusterList.remove(i--); } else if( ((ArrayList)((VertexView)clusterList.get(i)).getAttributes().get(KEY_CLUSTERED_VERTICES)).size() == 0 ){ clusterList.remove(i--); } } //remove clustered vertices from cellList for( int i = 0; i < cellsToCluster.size(); i++ ) cellList.remove(cellsToCluster.get(i)); //adding clusters to applyCellList and cellList for( int i = 0; i < clusterList.size(); i++ ){ applyCellList.add(clusterList.get(i)); cellList.add(clusterList.get(i)); } //storing a copy of position, to move vertices while declustering for( int i = 0; i < clusterList.size(); i++ ){ VertexView cluster = (VertexView) clusterList.get(i); Map attribs = cluster.getAttributes(); Point2D.Double clusterPos = (Point2D.Double) attribs.get(KEY_POSITION); attribs.put(KEY_CLUSTER_INIT_POSITION, new Point2D.Double( clusterPos.x, clusterPos.y)); } for( int i = 0; i < clusterList.size(); i++ ){ VertexView cluster = (VertexView)clusterList.get(i); cluster.setCachedBounds(getBoundingBox((ArrayList)cluster.getAttributes().get(KEY_CLUSTERED_VERTICES))); } colorizeClusters(clusterList); stop(20); } /******************************************************************************/ /** * Moves a vertice from the cluster, it is holded, to another cluster. This * implies that the vertice is removed from the old cluster and added to the * new. After this, the positions of the old and the new cluster are * recalculated. * * @param vertice Vertex that should be moved * @param cluster Cluster the vertex should be moved */ protected void moveVerticeToCluster(VertexView vertice, VertexView cluster){ //adding vertice to new cluster if( !cluster.getAttributes().containsKey(KEY_CLUSTERED_VERTICES) ) cluster.getAttributes().put(KEY_CLUSTERED_VERTICES,new ArrayList()); ArrayList clusteredVertices = (ArrayList) cluster.getAttributes().get(KEY_CLUSTERED_VERTICES); clusteredVertices.add(vertice); //removing vertice from old cluster if( vertice.getAttributes().containsKey(KEY_CLUSTER) ){ VertexView oldCluster = (VertexView) vertice.getAttributes().get(KEY_CLUSTER); ArrayList list = (ArrayList)oldCluster.getAttributes().get(KEY_CLUSTERED_VERTICES); list.remove(vertice); computeClusterPosition(oldCluster); } //register cluster in vertice vertice.getAttributes().put(KEY_CLUSTER,cluster); //reposition cluster computeClusterPosition(cluster); } /******************************************************************************/ /** * Recalculates the position of a cluster. The position of a cluster is defined * by the barycenter of the clustered vertices. * * @param cluster Cell, that has to be a cluster, should be repositioned. */ protected void computeClusterPosition(VertexView cluster){ ArrayList clusteredVertices = (ArrayList)cluster.getAttributes().get(KEY_CLUSTERED_VERTICES); Point2D.Double clusterPos = computeBarycenter(clusteredVertices); cluster.getAttributes().put(KEY_POSITION,clusterPos); } /******************************************************************************/ /** * Moves all clusters from {@link #cellList} and {@link #applyCellList}, * extracts their clustered vertices and adds them to {@link #cellList}. While * doing this, it repositions the clustered vertices with the move, the cluster * has made during the calculation. * * @see #clusterGraph() */ protected void declusterGraph(){ if( cellList.size() <= 1 ) return; //first collecting all clusters from applyCellList ArrayList clusterList = new ArrayList(); for( int i = 0; i < cellList.size(); i++ ){ VertexView cell = ((VertexView)cellList.get(i)); if( isCluster(cell) ) clusterList.add(cell); } if( clusterList.size() == 0 ) return; //cleaning up the cell lists for( int i = 0; i < clusterList.size(); i++ ){ cellList.remove(clusterList.get(i)); applyCellList.remove(clusterList.get(i)); } //repositioning and extracting vertices to cellList for( int i = 0; i < clusterList.size(); i++ ){ VertexView cluster = (VertexView)clusterList.get(i); Map attribs = cluster.getAttributes(); Point2D.Double newClusterPos = getPosition(cluster); Point2D.Double oldClusterPos = (Point2D.Double) attribs.get(KEY_CLUSTER_INIT_POSITION); //calculating move, cluster has made during his existance Point2D.Double move = new Point2D.Double(newClusterPos.x - oldClusterPos.x, newClusterPos.y - oldClusterPos.y); ArrayList vertexList = (ArrayList)attribs.get(KEY_CLUSTERED_VERTICES); //applying move to clustered vertices for( int j = 0; j < vertexList.size(); j++ ){ VertexView cell = (VertexView) vertexList.get(j); Point2D.Double cellPos = getPosition(cell); Point2D.Double newCellPos = new Point2D.Double(cellPos.x + move.x, cellPos.y + move.y); cell.getAttributes().put(KEY_POSITION,newCellPos); //refilling clustered vertices in cellList cellList.add(cell); } } } /******************************************************************************/ /** * Returns true when a cell is a cluster, else * false. A cell is a cluster when it has under it's * attributes a attribute with the boolean value true under * the corresponding key. * * @param cell cell, that should be researched wheather it is a cluster or not. * @return true if cell is a cluster, else * false. */ protected boolean isCluster(CellView cell){ if( cell.getAttributes().containsKey(KEY_IS_CLUSTER)){ if( isTrue((String)cell.getAttributes().get(KEY_IS_CLUSTER))){ return true; } else { System.err.println("FATAL ERROR: CELL CANNOT CLEARLY BE IDENTIFIED AS A CLUSTER!!!"); return false; } } else return false; } /******************************************************************************/ private void colorizeClusters(ArrayList clusterList){ Color[] colorList = new Color[] { Color.black, Color.magenta, Color.yellow, Color.blue, Color.green, Color.gray, Color.cyan, Color.red, Color.darkGray, Color.lightGray, Color.orange, Color.pink }; for( int i = 0; i < clusterList.size(); i++ ) if( i < colorList.length ){ ArrayList clusteredVertices = (ArrayList) ((CellView)clusterList.get(i)).getAttributes().get(KEY_CLUSTERED_VERTICES); showCellList(clusteredVertices,colorList[i]); } } /******************************************************************************/ private void showCellList(ArrayList list, Color color){ Map viewMap = new Hashtable(); for( int i = 0; i < list.size(); i++ ){ CellView view = (CellView)list.get(i); Point2D.Double pos = getPosition(i,list); Rectangle r = view.getBounds(); r.x = (int) (pos.getX() - (double)r.getWidth() /2.0); r.y = (int) (pos.getY() - (double)r.getHeight()/2.0); Object cell = view.getCell(); Map attributes = GraphConstants.createMap(); GraphConstants.setBackground(attributes, color); GraphConstants.setBounds (attributes, r); viewMap.put(cell, attributes); } jgraph.getGraphLayoutCache().edit(viewMap,null,null,null); } /******************************************************************************/ private synchronized void stop(double sec){ try{ wait((long)(sec * 1000.0)); } catch( Exception e ){ e.printStackTrace(); } } /******************************************************************************/ private void stop(int sec){ stop((double)sec); } }