/******************************************************************************* * Copyright (c) 2013 Arapiki Solutions Inc. * 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: * psmith - initial API and * implementation and/or initial documentation *******************************************************************************/ package com.buildml.eclipse.packages.layout; import java.util.ArrayList; import com.buildml.eclipse.packages.patterns.ActionPattern; import com.buildml.eclipse.packages.patterns.FileGroupPattern; import com.buildml.eclipse.utils.errors.FatalError; import com.buildml.model.IBuildStore; import com.buildml.model.IFileGroupMgr; import com.buildml.model.IPackageMemberMgr; import com.buildml.model.IPackageMemberMgr.MemberDesc; import com.buildml.model.IPackageMemberMgr.MemberLocation; import com.buildml.model.undo.ActionUndoOp; import com.buildml.model.undo.FileGroupUndoOp; import com.buildml.model.undo.MultiUndoOp; /** * A support object for managing the layout of members on a BuildML package diagram. * * @author Peter Smith <psmith@arapiki.com> */ public class LayoutAlgorithm { /*=====================================================================================* * FIELDS/TYPES *=====================================================================================*/ /** simple class for returning a member's row and col, within the auto-layout grid */ private class GridLocation { int col; int row; } /** The minimum X coordinate for a position on this diagram */ private static final int MIN_X = 0; /** The maximum X coordinate for a position on this diagram */ private static final int MAX_X = 3000; /** For esthetic reasons, we leave this many pixels between pictograms */ private static final int X_PADDING = 50; /** The IBuildStore that contains the package information */ private IBuildStore buildStore; /** The IPackageMemberMgr that contains our package membership information */ private IPackageMemberMgr pkgMemberMgr; /** The IFileGroupMgr that contains our fileGroup membership information */ private IFileGroupMgr fileGroupMgr; /*=====================================================================================* * CONSTRUCTORS *=====================================================================================*/ /** * Create a new {@link LayoutAlgorithm} object. * @param buildStore The IBuildStore that contains the package information. */ public LayoutAlgorithm(IBuildStore buildStore) { this.buildStore = buildStore; this.pkgMemberMgr = buildStore.getPackageMemberMgr(); this.fileGroupMgr = buildStore.getFileGroupMgr(); } /*=====================================================================================* * PUBLIC METHODS *=====================================================================================*/ /** * For the specified package member, determine the range of X coordinates that this member's * pictogram can be moved to. This range will depend on the X coordinates of the neighbouring * package members. * * @param memberType One of TYPE_ACTION, TYPE_FILE_GROUP, etc. * @param memberId The file group that is being moved. * @return The left and right X coordinate bounds. */ public LeftRightBounds getMemberMovementBounds(int memberType, int memberId) { IPackageMemberMgr pkgMemberMgr = buildStore.getPackageMemberMgr(); int leftX = MIN_X, rightX = MAX_X; /* determine the list of neighbours on the right side of this file group */ MemberDesc[] rightNeighbours = pkgMemberMgr.getNeighboursOf( memberType, memberId, IPackageMemberMgr.NEIGHBOUR_RIGHT, false); for (int i = 0; i < rightNeighbours.length; i++) { if (rightNeighbours[i].x < rightX) { rightX = rightNeighbours[i].x; } } /* adjust the right margin to account for the width of the file group pictogram itself */ rightX = rightX - getSizeOfPictogram(memberType).getWidth(); /* * Now check the left neighbours, although be sure to account for the * width of each left neighbour. */ MemberDesc[] leftNeighbours = pkgMemberMgr.getNeighboursOf( memberType, memberId, IPackageMemberMgr.NEIGHBOUR_LEFT, false); for (int i = 0; i < leftNeighbours.length; i++) { if (leftNeighbours[i].x > leftX) { leftX = leftNeighbours[i].x; leftX += getSizeOfPictogram(leftNeighbours[i].memberType).getWidth(); } } /* just to be sure, make sure the left bound isn't now greater than the right bound */ if (leftX > rightX) { rightX = leftX; } return new LeftRightBounds(leftX, rightX); } /*-------------------------------------------------------------------------------------*/ /** * Return the (width, height) in pixel of the specified type of member. * @param memberType * @return The size (width, height) in pixels of the specified pictogram. */ public PictogramSize getSizeOfPictogram(int memberType) { if (memberType == IPackageMemberMgr.TYPE_ACTION) { return ActionPattern.getSize(); } else if (memberType == IPackageMemberMgr.TYPE_FILE_GROUP) { return FileGroupPattern.getSize(); } throw new FatalError("Unhandled pictogram type: " + memberType); } /*-------------------------------------------------------------------------------------*/ /** * @return The number of pixels in the X direction to pad between pictograms, to make * them look nicely spread out. */ public int getXPadding() { return X_PADDING; } /*-------------------------------------------------------------------------------------*/ /** * Given a package member (e.g. file group or action), ensure that all of its right-side * neighbours are moved (if necessary) to higher x-coordinates. The avoid the appearance * of backward-facing arrows. * * @param multiOp The multi-undo/redo operation to append "move" operations to. * @param memberType The type of member that we're starting the bumping from. * @param memberId The ID of the member (actionId, fileGroupId). * @param moveNow True if we should do the move operations now, or false to let the * undo/redo operation handle it later. */ public void bumpPictogramsRight(MultiUndoOp multiOp, int memberType, int memberId, boolean moveNow) { /* * Determine this member's current x coordinate (of it's right edge). This is * necessary to start the recursion process. */ MemberLocation location = pkgMemberMgr.getMemberLocation(memberType, memberId); if (location == null) { return; } /* our recursive helper does the rest... */ bumpPictogramsRightHelper(multiOp, memberType, memberId, moveNow, location.x, location.y); } /*-------------------------------------------------------------------------------------*/ /** * Auto-layout the members of the specified package using an algorithm that places * package members in an easy-to-understand format. That is, all members are placed * neatly into columns on the package diagram, with the right-most column containing * all of the terminal file groups (no actions depend on these groups). All connection * arrows must flow from left to right, crossing other arrows as little as possible. * * The result of calling this method is a set of undo/redo operations that will make * the necessary "move" operations happen. * * @param multiOp The multi-undo/redo operation to append "move" operations to. * @param pkgId The ID of the package to auto-layout. */ public void autoLayoutPackage(MultiUndoOp multiOp, int pkgId) { /* initialize the data structure we'll use for sorting package members */ ArrayList<ArrayList<MemberDesc>> layoutGrid = new ArrayList<ArrayList<MemberDesc>>(); /* * Start by obtaining the sub-set of this package's members that have no outputs. * These members will go in the right-most column (column 0). The recursively place * all of their left neighbours into columns 1, 2, 3, ... */ MemberDesc allMembers[] = pkgMemberMgr.getMembersInPackage( pkgId, IPackageMemberMgr.SCOPE_NONE, IPackageMemberMgr.TYPE_ANY); for (int i = 0; i < allMembers.length; i++) { MemberDesc member = allMembers[i]; if (member.memberType != IPackageMemberMgr.TYPE_FILE) { /* skip over empty file groups - they aren't to be displayed */ if (member.memberType == IPackageMemberMgr.TYPE_FILE_GROUP) { int fileGroupSize = fileGroupMgr.getGroupSize(member.memberId); if (fileGroupSize < 1) { continue; } } /* this member is to be displayed - add it to the grid */ MemberDesc rightNeighbours[] = pkgMemberMgr.getNeighboursOf( member.memberType, member.memberId, IPackageMemberMgr.NEIGHBOUR_RIGHT, false); if (rightNeighbours.length == 0) { addWithNeighboursToAutoLayoutGrid(layoutGrid, member, 0); } } } /* * All members are now in the layout grid, and are in the correct columns. Next, sort * the members in each column into a logical order and assign (x, y) positions. */ assignAutoLayoutLocations(layoutGrid); /* * Add the actual move operations to our multi-undo/redo operation. No changes * have actually taken place, but they will if our caller invokes this operation. */ recordAutoLayoutOperations(multiOp, layoutGrid); } /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * A helper method for bumpPictogramsRight. * * @param multiOp The multi-undo/redo operation to append "move" operations to. * @param memberType The type of member that we're starting the bumping from. * @param memberId The ID of the member (actionId, fileGroupId). * @param moveNow True if we should do the move operations now, or false to let the * undo/redo operation handle it later. * @param x The current member's x-coordinate. * @param y y-coordinate (not used for now) */ private void bumpPictogramsRightHelper(MultiUndoOp multiOp, int memberType, int memberId, boolean moveNow, int x, int y) { /* allow for the width of the pictogram, and some extra padding */ int rightEdgeX = x + getSizeOfPictogram(memberType).getWidth() + X_PADDING; /* determine this member's right-side neigbours */ MemberDesc neighbours[] = pkgMemberMgr.getNeighboursOf( memberType, memberId, IPackageMemberMgr.NEIGHBOUR_RIGHT, false); /* * For each neighbour, see if it's x-coordinate is left of us, and therefore needs * to be bumped right. */ for (int i = 0; i < neighbours.length; i++) { MemberDesc thisNeighbour = neighbours[i]; if (thisNeighbour.x < rightEdgeX) { /* bump */ if (moveNow) { /* silently ignore errors - in worst case, the member just won't move */ pkgMemberMgr.setMemberLocation(thisNeighbour.memberType, thisNeighbour.memberId, rightEdgeX, thisNeighbour.y); } /* add this move to the undo/redo history */ addMemberMoveToHistory( multiOp, thisNeighbour.memberType, thisNeighbour.memberId, thisNeighbour.x, thisNeighbour.y, rightEdgeX, thisNeighbour.y); /* now recursively traverse all of this neighbour's right-side neighbours */ bumpPictogramsRightHelper( multiOp, thisNeighbour.memberType, thisNeighbour.memberId, moveNow, rightEdgeX, thisNeighbour.y); } } } /*-------------------------------------------------------------------------------------*/ /** * Create the relevant undo/redo operation to indicate this member's change in location. * * @param multiOp The multi-undo/redo operation to append to. * @param memberType The type of this member (IPackageMemberMgr.TYPE_FILE_GROUP, etc). * @param memberId The member's ID number * @param oldX The member's old X coordinate. * @param oldY The member's old Y coordinate. * @param newX The member's new X coordinate. * @param newY The member's old Y coordinate. */ private void addMemberMoveToHistory(MultiUndoOp multiOp, int memberType, int memberId, int oldX,int oldY, int newX, int newY) { if (memberType == IPackageMemberMgr.TYPE_ACTION) { ActionUndoOp op = new ActionUndoOp(buildStore, memberId); op.recordLocationChange(oldX, oldY, newX, newY); multiOp.add(op); } else if (memberType == IPackageMemberMgr.TYPE_FILE_GROUP) { FileGroupUndoOp op = new FileGroupUndoOp(buildStore, memberId); op.recordLocationChange(oldX, oldY, newX, newY); multiOp.add(op); } else { throw new FatalError("Invalid memberType"); } } /*-------------------------------------------------------------------------------------*/ /** * Helper method for autoLayoutPackage() to recursively add package members into the * layout grid. Neighbouring members are added to consecutive columns in the layout * grid so that they're appearing in neighbouring columns on the diagram. * * @param layoutGrid The layout grid that we're populating. * @param member The member to be added. * @param colNum The column within the grid to add the member to. */ private void addWithNeighboursToAutoLayoutGrid( ArrayList<ArrayList<MemberDesc>> layoutGrid, MemberDesc member, int colNum) { /* * if this member is already in the layout grid, and its further to the right * than our current column, remove it from it's existing location so that we * can place it in its new location. This gives the effect of placing members * in one column left of its left-most right-side neighbour, therefore avoiding * arrows that point backwards. */ GridLocation loc = findMemberInAutoLayoutGrid(layoutGrid, member); if ((loc != null) && (loc.col < colNum)) { removeMemberFromAutoLayoutGrid(layoutGrid, loc.col, loc.row); } /* * If it's not already somewhere in the current column, add this member to the end of * this column. Take care to add the column if it doesn't yet exist. */ if ((loc == null) || (loc.col < colNum)) { if (colNum >= layoutGrid.size()) { layoutGrid.add(colNum, new ArrayList<MemberDesc>()); } layoutGrid.get(colNum).add(member); } /* * Now, fetch all of this member's left neighbours, adding them to colNum + 1. * Note that since colNum == 0 implies the right most column, we are adding * neighbours in a left-ward direction. */ MemberDesc leftNeighbours[] = pkgMemberMgr.getNeighboursOf( member.memberType, member.memberId, IPackageMemberMgr.NEIGHBOUR_LEFT, false); if (leftNeighbours == null) { return; } for (int i = 0; i < leftNeighbours.length; i++) { addWithNeighboursToAutoLayoutGrid(layoutGrid, leftNeighbours[i], colNum + 1); } } /*-------------------------------------------------------------------------------------*/ /** * Helper method for autoLayoutPackage() that locates a specific member within the * layout grid, returning its (col, row) position. * * @param layoutGrid The layout grid containing all of the members. * @param member The member to search for (only memberType and memberId are used for * comparison). * @return The (col, row) location where the member was found, else null if the member * is not in the layout grid. */ private GridLocation findMemberInAutoLayoutGrid( ArrayList<ArrayList<MemberDesc>> layoutGrid, MemberDesc member) { /* for now, this is an O(n*n) algorithm, but that's OK since packages are kept small */ int numCols = layoutGrid.size(); /* for each column... */ for (int col = 0; col != numCols; col++) { ArrayList<MemberDesc> column = layoutGrid.get(col); int numRows = column.size(); /* for each row in that column... */ for (int row = 0; row != numRows; row++) { MemberDesc cell = column.get(row); /* if this member matches what we're searching for, return (col, row) */ if ((cell.memberId == member.memberId) && (cell.memberType == member.memberType)) { GridLocation location = new GridLocation(); location.col = col; location.row = row; return location; } } } return null; } /*-------------------------------------------------------------------------------------*/ /** * Helper method for autoLayoutPackage() that removes a member from a specified (col, row) * location within the grid. If the (col, row) parameters are out of range, no change is * made to the layout grid. * * @param layoutGrid The layout grid containing all of the members. * @param col The column of the member to remove. * @param row The row of the member to remove. */ private void removeMemberFromAutoLayoutGrid( ArrayList<ArrayList<MemberDesc>> layoutGrid, int col, int row) { if (col >= layoutGrid.size()) { return; } ArrayList<MemberDesc> column = layoutGrid.get(col); if (row >= column.size()) { return; } column.remove(row); } /*-------------------------------------------------------------------------------------*/ /** * Helper method for autoLayoutPackage() that assigns (x, y) coordinates for each member. * This method encapsulates the "smarts" of the layout algorithm. The outcome is that each * member has it's "x" and "y" fields adjusted accordingly. * * @param layoutGrid The layout grid containing all of the members. */ private void assignAutoLayoutLocations(ArrayList<ArrayList<MemberDesc>> layoutGrid) { /* * These initial calculations are done to ensure that all member types can fit into * a fixed-sized column. "colWidth" is the width that will incorporate all of the * members. We also need to center the members within that column, so "fileGroupOffset" * is used as an offset for positioning file groups. */ int actionWidth = getSizeOfPictogram(IPackageMemberMgr.TYPE_ACTION).getWidth(); int actionHeight = getSizeOfPictogram(IPackageMemberMgr.TYPE_ACTION).getHeight(); int fileGroupWidth = getSizeOfPictogram(IPackageMemberMgr.TYPE_FILE_GROUP).getWidth(); int fileGroupHeight = getSizeOfPictogram(IPackageMemberMgr.TYPE_FILE_GROUP).getHeight(); int colWidth = (int)((actionWidth > fileGroupWidth ? actionWidth : fileGroupWidth) * 1.2); int rowHeight = (actionHeight > fileGroupHeight) ? actionHeight : fileGroupHeight; int fileGroupOffset = (actionWidth - fileGroupWidth) / 2; /* * Now, we proceed to assign x-coordinates for each members, based on its column. For later * on, we also remember which of the columns is the "tallest". */ int numCols = layoutGrid.size(); if (numCols == 0) { return; } int tallestColumn = -1; int tallestColumnSize = -1; for (int i = 0; i != numCols; i++) { /* determine the x coordinate of the column so that members are nicely lined up. */ int colX = colWidth * (numCols - i - 1); ArrayList<MemberDesc> colMembers = layoutGrid.get(i); int numRows = colMembers.size(); /* is this the column (or one of the columns) that has the most members in it? */ if (numRows > tallestColumnSize) { tallestColumn = i; tallestColumnSize = numRows; } /* set all members in the column to the same x coordinate */ for (int j = 0; j < numRows; j++) { MemberDesc member = colMembers.get(j); member.x = colX; /* center file groups */ if (member.memberType == IPackageMemberMgr.TYPE_FILE_GROUP) { member.x += fileGroupOffset; } } } /* * Now assign y coordinates. This is done by evenly spacing the members across * each column. For example, if there are two members in a row, they're positioned * at 1/3rd and 2/3rds of the way. * TODO: we need a force-based algorithm for positioning members within a column. */ int diagramHeight = tallestColumnSize * rowHeight; for (int i = 0; i != numCols; i++) { ArrayList<MemberDesc> colMembers = layoutGrid.get(i); int numRows = colMembers.size(); /* evenly space out the members of the column */ for (int j = 0; j < numRows; j++) { MemberDesc member = colMembers.get(j); member.y = (int) (((j + 1) / (double)(numRows + 1)) * diagramHeight); } } } /*-------------------------------------------------------------------------------------*/ /** * Helper method for autoLayoutPackage() to appropriately position members within the * layout grid, and set their (x, y) coordinates, we generate all the necessary "move" operations * to actually move each member to its new location. * * @param multiOp The multi-undo/redo operation to add all the move operations to. * @param layoutGrid The layout grid that contains all the members. */ private void recordAutoLayoutOperations(MultiUndoOp multiOp, ArrayList<ArrayList<MemberDesc>> layoutGrid) { /* for each column in the layout grid... */ int numCols = layoutGrid.size(); for (int i = 0; i != numCols; i++) { ArrayList<MemberDesc> colMembers = layoutGrid.get(i); /* for each row in each column... */ for (int j = 0; j < colMembers.size(); j++) { MemberDesc member = colMembers.get(j); /* get the member's current (x, y) location */ MemberLocation currentLoc = pkgMemberMgr.getMemberLocation(member.memberType, member.memberId); /* create an operation to move the member to it's new (x, y) location */ if (currentLoc != null) { addMemberMoveToHistory(multiOp, member.memberType, member.memberId, currentLoc.x, currentLoc.y, member.x, member.y); } } } } /*-------------------------------------------------------------------------------------*/ }