/******************************************************************************
* Copyright (c) 2011-2013, Linagora
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Linagora - initial API and implementation
*******************************************************************************/
package com.ebmwebsourcing.petals.services.eip.designer.helpers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import com.ebmwebsourcing.petals.services.eip.PetalsEipPlugin;
import com.ebmwebsourcing.petals.services.eip.designer.model.AbstractNode;
import com.ebmwebsourcing.petals.services.eip.designer.model.EipConnection;
import com.ebmwebsourcing.petals.services.eip.designer.model.EipNode;
import com.ebmwebsourcing.petals.services.eip.designer.model.Endpoint;
/**
* A class which computes various statistics about a sub-graph of an EIP chain.
* <p>
* The root of such a sub-graph is an EIP node with no incoming connection.
* The leaves of a sub-graph should be end-points.
* </p>
*
* @author Vincent Zurczak - EBM WebSourcing
*/
public final class SubGraphStatistics {
/**
* Considering columns of nodes (i.e. levels in the tree), this
* value indicates the horizontal padding between the two node areas.
*/
private final static int GRID_PADDING_X = 240;
/**
* Considering nodes on a same column (i.e. same level in the tree), this
* value indicates the vertical padding between the two node areas.
*/
private final static int GRID_PADDING_Y = 40;
/**
* The default height of a node area (deduced approximately from the figure height).
* <p>
* To use if and only if the figure's size could not be retrieved.
* </p>
*/
private final static int GRID_CELL_DEFAULT_HEIGHT = 70;
/**
* The default width of a node area (deduced approximately from the figure height).
* <p>
* To use if and only if the figure's size could not be retrieved.
* </p>
*/
private final static int GRID_CELL_DEFAULT_WIDTH = 90;
/**
* A map that associates nodes with figures.
*/
private final Map<AbstractNode,IFigure> nodeToFigure;
/**
* A map to associate a node with a tree level.
*/
private final Map<AbstractNode,Integer> nodeToLevel;
/**
* A map that associates a node (keys) with a computed Y coordinate.
*/
private final Map<AbstractNode,Integer> nodeToCoY;
/**
* A map that associates a tree level (keys) with a width (same width for all the nodes of this level).
* <p>
* Iterating through this map is done in the natural level order.
* The first key will always be 1.
* </p>
*/
private final TreeMap<Integer,Integer> levelToWidth;
/**
* A map that associates a tree level (keys) with a ordered list of nodes.
* <p>
* Iterating through this map is done in the natural level order.
* The first key will always be 1.
* </p>
*/
private final TreeMap<Integer,ArrayList<AbstractNode>> levelToNodes;
/**
* A map to associate a node with a relative location (if the sub-graph was the entire EIP chain).
*/
private final Map<AbstractNode,Point> nodeToRelativeLocation;
/**
* The size of the sub-graph.
*/
private Dimension subGraphDimension;
/**
* Constructor.
* @param rootNode
* @param nodeToFigure
*/
public SubGraphStatistics( AbstractNode rootNode, Map<AbstractNode,IFigure> nodeToFigure ) {
this.nodeToFigure = nodeToFigure;
this.nodeToLevel = new HashMap<AbstractNode,Integer> ();
this.nodeToCoY = new HashMap<AbstractNode,Integer> ();
this.levelToWidth = new TreeMap<Integer,Integer> ();
this.levelToNodes = new TreeMap<Integer,ArrayList<AbstractNode>> ();
this.nodeToRelativeLocation = new HashMap<AbstractNode,Point> ();
computeNodeStatistics( rootNode, 1, 0 );
adjustYCoordinates();
computeNodeRelativeLocations();
}
/**
* @return the number of nodes in this sub-graph
*/
public int getNodeCount() {
return this.nodeToLevel.size();
}
/**
* @return the nodeToRelativeLocation
*/
public Map<AbstractNode,Point> getNodeToRelativeLocation() {
return this.nodeToRelativeLocation;
}
/**
* @return the subGraphDimension
*/
public Dimension getSubGraphDimension() {
return this.subGraphDimension;
}
/**
* Adjusts the Y coordinate to prevent node overlap.
* <p>
* Here is a situation where this method is required.
*
* <code>EIP_____ EIP 1 ______ edpt
* \____ EIP 2 ______ edpt</code>
*
* With the single algorithm, EIP 1 and 2 will overlap vertically.
* </p>
*/
private void adjustYCoordinates() {
// 1st pass: correct the padding and suppress overlap
List<EipNode> zeroNodes = new ArrayList<EipNode> ();
Map<AbstractNode,Integer> nodeToNewOffset = new HashMap<AbstractNode,Integer> ();
for( ArrayList<AbstractNode> nodes : this.levelToNodes.values()) {
int maxY = 0;
for( AbstractNode node : nodes ) {
// Find the original Y
int y = this.nodeToCoY.get( node );
// The initial offset is inherited from the parent
int offset = 0;
if( node.getIncomingConnection() != null )
offset = nodeToNewOffset.get( node.getIncomingConnection().getSource());
// Prevent overlap
if( y <= maxY )
offset = maxY - y;
// Make sure there is the minimal vertical padding
if( maxY != 0 && y - maxY < GRID_PADDING_Y )
offset += GRID_PADDING_Y - y + maxY;
// Store the new Y
y += offset;
nodeToNewOffset.put( node, offset );
this.nodeToCoY.put( node, y );
// Update the maximal Y for this level
maxY = y + getFigureSize( node ).height;
// Something for the next pass?
if( offset == 0 && node instanceof EipNode )
zeroNodes.add((EipNode) node);
}
}
// 2nd pass: there may be nodes that were not translated (e.g. the root node).
// This pass moves them at the middle-Y...
for( EipNode eip : zeroNodes ) {
int y = computeCenteredYCoordinate( eip, getFigureSize( eip ));
this.nodeToCoY.put( eip, y );
}
}
/**
* Computes the location of all the nodes of this sub-graph.
* <p>
* Locations are considered ideal. It means that they are set as if the sub-graph
* was the entire EIP chain. Real locations will obtained from the relative locations
* by a translation. The translation depends on the offsets to display a sub-graph
* in the entire area of the EIP chain.
* </p>
*/
private void computeNodeRelativeLocations() {
// Compute the X coordinate for every level
Map<Integer,Integer> levelToCoX = new HashMap<Integer,Integer> ();
int x = 0;
for( Map.Entry<Integer,Integer> entry : this.levelToWidth.entrySet()) {
if( x != 0 )
x += GRID_PADDING_X;
else
x = 30;
levelToCoX.put( entry.getKey(), x );
x += entry.getValue();
}
// Compute the location for every node
int maxX = 0, maxY = 0;
for( Map.Entry<AbstractNode,Integer> entry : this.nodeToLevel.entrySet()) {
int level = this.nodeToLevel.get( entry.getKey());
x = levelToCoX.get( level );
int y = this.nodeToCoY.get( entry.getKey());
this.nodeToRelativeLocation.put( entry.getKey(), new Point( x, y ));
if( x > maxX )
maxX = x;
if( y > maxY )
maxY = y;
}
// Store the size of the sub-graph
this.subGraphDimension = new Dimension( maxX, maxY );
}
/**
* Computes the level and the coordinates for a node of the sub-graph.
* <p>
* The algorithm relies on the following predicates:
* </p>
* <ol>
* <li>The X coordinate of a node depends on the level and the width of the previous levels.</li>
* <li>The width of a level depends on the width of the biggest figure of this level.</li>
* <li>The Y coordinate of a node depends on whether this node is a leaf or an intermediate node in the tree.</li>
* <li>The Y coordinate of a leaf is the highest computed Y + a specific padding + the height of the preceding figure.</li>
* <li>The Y coordinate of an intermediate node is the middle of the max and min Y of the node's children.</li>
* </ol>
* <p>
* Mathematically, it gives:
* </p>
* <ol>
* <li>X( node ) = X( node-1 ) + padding + Width( node-1 )</li>
* <li>Width( level ) = Math.max( Width( node )) where node is any node in the level / column.</li>
* <li>...</li>
* <li>Y( node ) = Math.max( Y( node2 )) where node2 is any node that has been processed before the current node.</li>
* <li>Y( node ) = [ Y( node_child_max ) + Y( node_child_min ) - Height( node ) ] / 2</li>
* </ol>
*
* @param node
* @param level the tree level of the node (1 for the root level)
* @param maxY the biggest computed Y coordinate (for any column or level)
* @return the new biggest computed Y coordinate (can be the same than the received one)
*/
private int computeNodeStatistics( AbstractNode node, int level, int maxY ) {
// Register this node
this.nodeToLevel.put( node, level );
ArrayList<AbstractNode> nodes = this.levelToNodes.get( level );
if( nodes == null )
nodes = new ArrayList<AbstractNode> ();
nodes.add( node );
this.levelToNodes.put( level, nodes );
// Get the size of this figure
Dimension dim = getFigureSize( node );
// Update the width of the tree level.
// The X coordinate of a node depends on the width for this level.
// This is why it can only be computed once the entire tree has been explored.
Integer levelWidth = this.levelToWidth.get( level );
if( levelWidth == null )
levelWidth = -1;
if( dim.width > levelWidth ) {
levelWidth = dim.width;
this.levelToWidth.put( level, levelWidth );
}
// Compute the Y coordinate of the node
// This last one depends on the Y coordinates of the children
// Note that there are only two sub-classes of AbstractNode
int y;
if( node instanceof Endpoint ) {
maxY += GRID_PADDING_Y;
y = maxY;
maxY += dim.height;
} else {
// Process all the children
for( EipConnection conn : ((EipNode) node).getOutgoingConnections())
maxY = computeNodeStatistics( conn.getTarget(), level + 1, maxY );
// Determine the Y coordinate of the current EIP
// The parent's location depends on the children
y = computeCenteredYCoordinate((EipNode) node, dim );
// No child found => treat it as an end-point
if( y == -1 ) {
maxY += GRID_PADDING_Y;
y = maxY;
maxY += dim.height;
}
}
// Store the Y coordinate for a later use
this.nodeToCoY.put( node, y );
return maxY;
}
/**
* @param node a node
* @return the size of the figure associated to this node
*/
private Dimension getFigureSize( AbstractNode node ) {
IFigure fig = this.nodeToFigure.get( node );
int width, height;
if( fig != null ) {
width = fig.getSize().width;
height = fig.getSize().height;
} else {
width = GRID_CELL_DEFAULT_WIDTH;;
height = GRID_CELL_DEFAULT_HEIGHT;
PetalsEipPlugin.log( "No figure was found for the node " + node.getId()
+ " (EIP chain: " + node.getEipChain().getTitle() + " ).", IStatus.ERROR );
}
return new Dimension( width, height );
}
/**
* Computes the Y coordinate so that the EIP is centered with respect to its children.
* @param eip the EIP
* @param dim the dimension of the figure associated to the EIP
* @return the new Y coordinate, -1 if there is no child
*/
private int computeCenteredYCoordinate( EipNode eip, Dimension dim ) {
// Find the minimal and maximal values
int childMinY = -1, childMaxY = -1;
AbstractNode lastNode = null;
for( EipConnection conn : eip.getOutgoingConnections()) {
lastNode = conn.getTarget();
int childY = this.nodeToCoY.get( lastNode );
childMaxY = childY;
if( childMinY == -1 )
childMinY = childY;
}
// Put it at the middle...
int y = childMaxY + childMinY;
// ... but take the figure's height in account.
// When there is only 1 child, the EIP and the child should be aligned
if( eip.getOutgoingConnections().size() == 1 ) {
Dimension childDim = getFigureSize( lastNode );
y -= dim.height - childDim.height;
} else if( eip.getOutgoingConnections().size() > 1 ) {
Dimension childDim = getFigureSize( lastNode );
y += childDim.height - dim.height;
}
y = y / 2;
return y;
}
}