package br.uff.ic.dyevc.graph.layout; //~--- non-JDK imports -------------------------------------------------------- import br.uff.ic.dyevc.exception.DyeVCException; import br.uff.ic.dyevc.graph.GraphDomainMapper; import br.uff.ic.dyevc.model.CommitInfo; import br.uff.ic.dyevc.model.MonitoredRepository; import br.uff.ic.dyevc.tools.vcs.git.CommonAncestorFinder; import edu.uci.ics.jung.algorithms.layout.AbstractLayout; import edu.uci.ics.jung.algorithms.shortestpath.DijkstraDistance; import edu.uci.ics.jung.algorithms.util.IterativeContext; import org.slf4j.LoggerFactory; //~--- JDK imports ------------------------------------------------------------ import java.awt.Dimension; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeSet; import javax.swing.JOptionPane; /** * Layout for drawing a repository history * * @author Cristiano * @param <V> The type of the vertices * @param <E> The type of the edges */ public class RepositoryHistoryLayout<V, E> extends AbstractLayout<V, E> implements IterativeContext { private V firstCommit = null; private boolean processed = false; /** X distance between two nodes */ public static final double XDISTANCE = 70.0; /** Y distance between two branches */ public static final double YDISTANCE = 70.0; private static final double EPSILON = 0.000001D; /** * List of heads found in the repository (commits with no children) */ private final ArrayList<V> heads = new ArrayList<V>(); /** * List of heads that were already processed */ private final ArrayList<V> processedHeads = new ArrayList<V>(); /** * List of heights for each node. To find the height of a node, all that is * needed is to divide the X position by XDISTANCE and the integer result is * the position in the list where the node's height is stored. */ private List<Integer> heights; /** * Stores the nodes in the order corresponding to its X position */ private final ArrayList<V> nodes = new ArrayList<V>(); /** * Maps each node's hash info with its X position */ private final HashMap<String, Double> nodePositions = new HashMap<String, Double>(); /** * DijkstraDistance is used to find out if there is a path between two nodes. */ private final DijkstraDistance<V, E> distances; /** * Repository from where this log is being drawn */ MonitoredRepository rep; /** * Map of all commits found in topology, keyed by its hash. */ Map<String, CommitInfo> commitInfoMap; /** * Creates an instance for the specified graph. * @param mapper The {@link #GraphDomainMapper} to be used * @param rep The monitored repository to create the layout to * @param size the size of the layout */ public RepositoryHistoryLayout(GraphDomainMapper<Map<String, CommitInfo>> mapper, MonitoredRepository rep, Dimension size) { super(mapper.getGraph(), size); LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("Constructor -> Entry"); LoggerFactory.getLogger(RepositoryHistoryLayout.class).debug( "Constructor -> Graph has {} nodes to be plotted.", graph.getVertexCount()); this.rep = rep; this.commitInfoMap = mapper.getDomain(); distances = new DijkstraDistance<V, E>(graph); LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("Constructor -> Exit"); } @Override public void reset() { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("reset -> Entry"); doInit(); LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("reset -> Exit"); } @Override public void initialize() { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("initialize -> Entry"); if (!processed) { doInit(); processed = true; } LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("initialize -> Exit"); } public void initialize(boolean force) { if (force) { processed = false; initialize(); } } public int getWidth() { return graph.getVertexCount() * (int)XDISTANCE; } private void doInit() { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("doInit -> Entry"); // SplashScreen splash = SplashScreen.getInstance(); try { // Starting X position double xPos = 0.0; heads.clear(); processedHeads.clear(); nodes.clear(); // splash.setStatus("Calculating X positions..."); // splash.setVisible(true); calcXPositionsAndFindHeads(xPos); LoggerFactory.getLogger(RepositoryHistoryLayout.class).debug("doInit -> Graph has {} nodes and {} heads", nodes.size(), heads.size()); LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace( "doInit -> Initializing visited state for all graph nodes"); // splash.setStatus("Resetting visited status for " + graph.getVertexCount() + " vertices..."); for (V v : graph.getVertices()) { // resets the attribute "visited" of each node to repaint graph uppon user demand if (v instanceof CommitInfo) { ((CommitInfo)v).setVisited(false); } } LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace( "doInit -> Finished initializing visited state for all graph nodes"); int i = 1; while (!heads.isEmpty()) { // splash.setStatus("Calculating Y positions starting in head " + i++ + "/" + heads.size() + "..."); // Initial height of tree int height = 0; V v = heads.remove(heads.size() - 1); // Get max height for (V head : processedHeads) { height = Math.max(height, findMaxHeightBetweenSubtrees(v, head)); } processedHeads.add(v); // There is no problem in starting with height = 0, because the // algorithm will change it if necessary calcYPositions(v, height); } // splash.setVisible(false); } catch (DyeVCException vcse) { JOptionPane.showMessageDialog( null, "Application received the following exception trying to show repository commit history:\n" + vcse + "\n\nOpen console window to see error details.", "Error found!", JOptionPane.ERROR_MESSAGE); } catch (RuntimeException ex) { ex.printStackTrace(System.err); JOptionPane.showMessageDialog( null, "Application received the following exception trying to show repository commit history:\n" + ex + "\n\nOpen console window to see error details.", "Error found!", JOptionPane.ERROR_MESSAGE); } finally { // splash.dispose(); } LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("doInit -> Exit"); } /** * Calculates the X position for each node. Positions are calculated starting * from the first commit, which will be placed at X position equals zero. * then, each subsequent node is placed to the right of the previous one. * When a split is found, then the dates of each commit are used to determine * which one will be plotted first.<br> * This is to give a cronological order of the commits. The dates are not used * solely because the repositories are distributed, which can lead to different * clocks being used, with no central time predefined. <br> * * The usage of the dates to determine the order in splits is implicit, as * the CommitInfo implements the Comparable interface using the commit date as * the comparison attribute. * @param xPos */ private void calcXPositionsAndFindHeads(double xPos) { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("calcXPositionsAndFindHeads -> Entry."); LoggerFactory.getLogger(RepositoryHistoryLayout.class).debug( "calcXPositionsAndFindHeads -> Initial xPos is <{}>", xPos); // Initializes the heights list with -1 for each node, meaning that the // height was not calculated yet Integer heightsArray[] = new Integer[graph.getVertexCount()]; Arrays.fill(heightsArray, new Integer(-1)); heights = Arrays.asList(heightsArray); // Gets the first commit and adds it to the list of nodes to be processed. TreeSet<V> nodesToProcess = new TreeSet<V>(); nodesToProcess.add(getFirstCommit()); // Leave the loop if there is no node to process while (!nodesToProcess.isEmpty()) { // As the nodes natural order are based on the commit's date, first // node will be the most ancient commit V v = nodesToProcess.first(); nodesToProcess.remove(v); // In the case of a merge, the v will be found more than once. // If this happens, v will be shifted to a new X position and all nodes // that were after it before will be left shifted. if (nodes.contains(v)) { double newXPos = xPos - (XDISTANCE * 2); for (int i = nodes.indexOf(v); i < nodes.size(); i++) { // ajustar a posição x, recuando em xdistance V node = nodes.get(i); Point2D coords = transform(node); coords.setLocation(newXPos, coords.getY()); nodePositions.put(getHashFromNode(node), new Double(newXPos)); } xPos -= XDISTANCE; nodes.remove(v); } Point2D xyd = transform(v); xyd.setLocation(xPos, xyd.getY()); nodePositions.put(getHashFromNode(v), new Double(xPos)); nodes.add(v); xPos += XDISTANCE; int childrenCount = graph.getPredecessorCount(v); if (childrenCount == 0) { // A new head was found. Include it in the list, because Y position // is calculated starting with the heads. heads.add(v); } else { // Include predecessors in the Set to be processed, taking care of // its type, because processed graphs will not be processed here. for (V child : graph.getPredecessors(v)) { if (child instanceof CommitInfo) { nodesToProcess.add(child); } } } } LoggerFactory.getLogger(RepositoryHistoryLayout.class).debug("calcXPositionsAndFindHeads Final xPos is <{}>", xPos); LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("calcXPositionsAndFindHeads -> Exit."); } /** * Calculates the Y position for each vertex of the graph * * @param v Initial vertex of subtree to calculate height * @param childHeight Initial height of subtree * @return true if height is different from initial childHeight */ protected synchronized boolean calcYPositions(V v, int childHeight) { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("calcYPositions -> Entry"); LoggerFactory.getLogger(RepositoryHistoryLayout.class).debug( "calcYPositions -> Will now calculate yPos for node <{}>, which initially received childHeight <{}>", ((CommitInfo)v).getHash(), childHeight); boolean result = false; boolean visited = false; while (!visited) { // Visits each node only once if (v instanceof CommitInfo) { CommitInfo ci = (CommitInfo)v; visited = ci.isVisited(); if (!visited) { ci.setVisited(true); Point2D xyd = transform(v); /* * Verifies if there is any node with height greater or equals * to childHeight, between v and its sucessors and predecessors, * and gets the highest number to be used as v's height */ int height = Math.max(childHeight, findMaxHeightBetweenSuccessors(v, childHeight)); height = Math.max(height, findMaxHeightBetweenPredecessors(v, height)); xyd.setLocation(xyd.getX(), YDISTANCE * height); if (childHeight != height) { result = true; V nodeToCheck = v; while (isOnlyParentAndChild(nodeToCheck)) { /* * If node has only one child and is the only parent of this child, then * children before it should be at the same height until a merge is found */ V child = graph.getPredecessors(nodeToCheck).iterator().next(); Point2D xydChild = transform(child); xydChild.setLocation(xydChild.getX(), xyd.getY()); nodeToCheck = child; } } childHeight = height; heights.set(calcIndexFromXPosition(v), childHeight); int parentsCount = graph.getSuccessorCount(v); if (parentsCount == 1) { // only one parent, process it using the same childHeight v = graph.getSuccessors(v).iterator().next(); } else { // more parents -> process each one, increasing height for each one of them // i is initially -1 because the first subtree will be at same height as its child int i = -1; for (V parent : graph.getSuccessors(v)) { i++; boolean heightChanged = calcYPositions(parent, childHeight + i); if (heightChanged) { // if height was changed during parent's processing, // update it to make sure that further parents do not to collide. childHeight = getHeightFromXPosition(parent); } } } } } } LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("calcYPositions -> Exit"); return result; } /** * Calculates the max height between v and its most ancient successor, not * including them. * * @param v Node for which max height will be calculated * @return max height found */ private int findMaxHeightBetweenSuccessors(V v, int height) { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenSuccessors -> Entry"); int vIndex = calcIndexFromXPosition(v); int minIndex = vIndex; int result = -1; for (V parent : graph.getSuccessors(v)) { minIndex = Math.min(minIndex, calcIndexFromXPosition(parent)); } minIndex++; if (minIndex >= vIndex) { // There is no node situated between v and its most ancient successor LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenSuccessors -> Exit"); return height; } List<Integer> heightsSubList = heights.subList(minIndex, vIndex); List<V> nodesSubList = nodes.subList(minIndex, vIndex); if (heightsSubList.isEmpty()) { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenSuccessors -> Exit"); return height; } else { for (V node : nodesSubList) { if (getHeightFromXPosition(node) > -1) { // Node was already processed and its height was calculated if ((distances.getDistance(v, node)) == null) { // if there is no path from node to v, then v is in a different // branch from node and thus must be assigned a higher height than node int nonSuccessorHeight = getHeightFromXPosition(node); result = (nonSuccessorHeight > result) ? nonSuccessorHeight : result; } } } LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenSuccessors -> Exit"); return result + 1; } } /** * Calculates the max height between v and its most ancient predecessor, not * including them. * * @param v Node for which max height will be calculated * @return */ private int findMaxHeightBetweenPredecessors(V v, int height) { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenPredecessors -> Entry"); int vIndex = calcIndexFromXPosition(v); int maxIndex = vIndex; int result = -1; for (V child : graph.getPredecessors(v)) { maxIndex = Math.max(maxIndex, calcIndexFromXPosition(child)); } maxIndex--; if (maxIndex <= vIndex) { // There is no node situated between v and its most ancient successor LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenPredecessors -> Exit"); return height; } List<Integer> heightsSubList = heights.subList(vIndex, maxIndex); List<V> nodesSubList = nodes.subList(vIndex, maxIndex); if (heightsSubList.isEmpty()) { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenPredecessors -> Exit"); return height; } else { for (V node : nodesSubList) { if (getHeightFromXPosition(node) > -1) { if ((distances.getDistance(node, v)) == null) { // if there is no path from node to v, then v should be on a greater height than node int nonPredecessorHeight = getHeightFromXPosition(node); result = (nonPredecessorHeight > result) ? nonPredecessorHeight : result; } } } LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenPredecessors -> Exit"); return result + 1; } } /** * Calculates the max height between subtrees headed by v1 and v2, until their * merge base * * @param head1 First head * @param head2 Second head * @return the max height of nodes between nodes v1 and v2 */ private int findMaxHeightBetweenSubtrees(V head1, V head2) throws DyeVCException { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenSubtrees -> Entry"); int index1 = calcIndexFromXPosition(head1); CommonAncestorFinder caf = new CommonAncestorFinder(commitInfoMap); CommitInfo commit = caf.getCommonAncestor(getHashFromNode(head1), getHashFromNode(head2)); String hashBase = commit.getHash(); int index2 = calcIndexFromXPosition(nodePositions.get(hashBase)); if (index1 > index2) { int temp = index1; index1 = index2; index2 = temp; } List<Integer> heightsSubList = heights.subList(index1, index2); int result = Collections.max(heightsSubList); LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("findMaxHeightBetweenSubtrees -> Exit"); return result + 1; } /** * Calculates the position in the heights list, based on X position. The * calculation involves dividing the X position by XDISTANCE. * * @param v Node for which position will be calculated. * @return */ private int calcIndexFromXPosition(V v) { return calcIndexFromXPosition(transform(v).getX()); } /** * Calculates the position in the heights list, based on a node's hash * * @param hash Node's hash for which position will be calculated. * @return */ private int calcIndexFromHash(String hash) { return calcIndexFromXPosition(nodePositions.get(hash)); } /** * Calculates the position in the heights list, based on X position. The * calculation involves dividing the X position by XDISTANCE. * * @param xPosition Position to be calculated * @return */ private int calcIndexFromXPosition(Double xPosition) { return (int)(xPosition / XDISTANCE); } /** * Verifies if node v has only one child node and is the only parent of this * child node * * @param v Node to be checked * @return true, if node v has only one child and is the only parent of this * child node */ private boolean isOnlyParentAndChild(V v) { int childrenCount = graph.getPredecessorCount(v); boolean isOnlyParentAndChild = (childrenCount == 1) && (graph.getSuccessorCount(graph.getPredecessors(v).iterator().next()) == 1); return isOnlyParentAndChild; } /** * Gets the height of a node, based on its X Position * * @param node Node to retrieve the height for * @return node's height */ private int getHeightFromXPosition(V node) { return heights.get(calcIndexFromXPosition(node)).intValue(); } /** * Returns the first commit in the graph (node with no successors) * * @return the first commit in the graph; */ public V getFirstCommit() { LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("getFirstCommit -> Entry"); if (firstCommit == null) { for (V v : graph.getVertices()) { if (graph.getSuccessorCount(v) == 0) { firstCommit = v; break; } } } LoggerFactory.getLogger(RepositoryHistoryLayout.class).debug( "getFirstCommit -> First commit for this repository has id <{}>", ((CommitInfo)firstCommit).getHash()); LoggerFactory.getLogger(RepositoryHistoryLayout.class).trace("getFirstCommit -> Exit"); return firstCommit; } protected boolean Equals(double a, double b) { return Math.abs(a - b) < EPSILON; } /** * This one is an incremental visualization. * @return Always true (this layout implements a incremental visualization) */ public boolean isIncremental() { return true; } /** * Returns true once the current iteration has passed the maximum count, * <tt>MAX_ITERATIONS</tt>. * @return Always true. */ @Override public boolean done() { return true; } @Override public void step() {} /** * Gets the hash of a node * @param node The node to get the hash * @return the hash of the specified node */ private String getHashFromNode(V node) { return ((CommitInfo)node).getHash(); } }