blob: 14fd775f8ddc830ddf357b1127a177259a8647e2 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.rendering.multi;
import com.android.annotations.Nullable;
import com.intellij.util.ui.UIUtil;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* This class implements 2D bin packing: packing rectangles into a given area as
* tightly as "possible" (bin packing in general is NP hard, so this class uses
* heuristics).
* <p/>
* The algorithm implemented is to keep a set of (possibly overlapping)
* available areas for placement. For each newly inserted rectangle, we first
* pick which available space to occupy, and we then subdivide the
* current rectangle into all the possible remaining unoccupied sub-rectangles.
* We also remove any other space rectangles which are no longer eligible if
* they are intersecting the newly placed rectangle.
* <p/>
* This algorithm is not very fast, so should not be used for a large number of
* rectangles.
*/
@SuppressWarnings("AssignmentToForLoopParameter")
class BinPacker {
/**
* When enabled, the successive passes are dumped as PNG images showing the
* various available and occupied rectangles)
*/
static final boolean DEBUG = false;
/** Whether new rectangles should be placed on the left side of available rectangles */
private static final boolean PLACE_LEFT = true;
/** Whether new rectangles should be placed on the top rather than the bottom */
private static final boolean PLACE_TOP = true;
private final List<Rectangle> mySpace = new ArrayList<Rectangle>();
private final int myMinHeight;
private final int myMinWidth;
/**
* Creates a new {@linkplain BinPacker}. To use it, first add one or more
* initial available space rectangles with {@link #addSpace(Rectangle)}, and then
* place the rectangles with {@link #occupy(int, int)}. The returned
* {@link Rectangle} from {@link #occupy(int, int)} gives the coordinates of the
* positioned rectangle.
*
* @param minWidth the smallest width of any rectangle placed into this bin
* @param minHeight the smallest height of any rectangle placed into this bin
*/
BinPacker(int minWidth, int minHeight) {
myMinWidth = minWidth;
myMinHeight = minHeight;
if (DEBUG) {
myAllocated = new ArrayList<Rectangle>();
}
}
/**
* Adds more available space
*/
void addSpace(Rectangle rect) {
if (rect.width >= myMinWidth && rect.height >= myMinHeight) {
mySpace.add(rect);
}
}
/**
* Attempts to place a rectangle of the given dimensions, if possible
*/
@Nullable
Rectangle occupy(int width, int height) {
int index = findBest(width, height);
if (index == -1) {
return null;
}
return split(index, width, height);
}
/**
* Finds the best available space rectangle to position a new rectangle of
* the given size in.
*/
private int findBest(int width, int height) {
if (mySpace.isEmpty()) {
return -1;
}
// Try to pack as far up as possible first
int bestIndex = -1;
boolean multipleAtSameY = false;
int minY = Integer.MAX_VALUE;
for (int i = 0, n = mySpace.size(); i < n; i++) {
Rectangle rect = mySpace.get(i);
if (rect.y <= minY) {
if (rect.width >= width && rect.height >= height) {
if (rect.y < minY) {
minY = rect.y;
multipleAtSameY = false;
bestIndex = i;
}
else if (minY == rect.y) {
multipleAtSameY = true;
}
}
}
}
if (!multipleAtSameY) {
return bestIndex;
}
bestIndex = -1;
// Pick a rectangle. This currently tries to find the rectangle whose shortest
// side most closely matches the placed rectangle's size.
// Attempt to find the best short side fit
int bestShortDistance = Integer.MAX_VALUE;
int bestLongDistance = Integer.MAX_VALUE;
for (int i = 0, n = mySpace.size(); i < n; i++) {
Rectangle rect = mySpace.get(i);
if (rect.y != minY) { // Only comparing elements at same y
continue;
}
if (rect.width >= width && rect.height >= height) {
if (width < height) {
int distance = rect.width - width;
if (distance < bestShortDistance || distance == bestShortDistance && (rect.height - height) < bestLongDistance) {
bestShortDistance = distance;
bestLongDistance = rect.height - height;
bestIndex = i;
}
}
else {
int distance = rect.width - width;
if (distance < bestShortDistance || distance == bestShortDistance && (rect.height - height) < bestLongDistance) {
bestShortDistance = distance;
bestLongDistance = rect.height - height;
bestIndex = i;
}
}
}
}
return bestIndex;
}
/**
* Removes the rectangle at the given index. Since the rectangles are in an
* {@link ArrayList}, removing a rectangle in the normal way is slow (it
* would involve shifting all elements), but since we don't care about
* order, this always swaps the to-be-deleted element to the last position
* in the array first, <b>then</b> it deletes it (which should be
* immediate).
*
* @param index the index in the {@link #mySpace} list to remove a rectangle
* from
*/
private void removeRect(int index) {
assert !mySpace.isEmpty();
int lastIndex = mySpace.size() - 1;
if (index != lastIndex) {
// Swap before remove to make deletion faster since we don't
// care about order
Rectangle temp = mySpace.get(index);
mySpace.set(index, mySpace.get(lastIndex));
mySpace.set(lastIndex, temp);
}
mySpace.remove(lastIndex);
}
/**
* Splits the rectangle at the given rectangle index such that it can contain
* a rectangle of the given width and height.
*/
private Rectangle split(int index, int width, int height) {
Rectangle rect = mySpace.get(index);
assert rect.width >= width && rect.height >= height : rect;
Rectangle r = new Rectangle();
if (PLACE_LEFT) {
r.x = rect.x;
} else {
r.x = rect.x + rect.width - width;
}
if (PLACE_TOP) {
r.y = rect.y;
} else {
r.y = rect.y + rect.height - height;
}
r.width = width;
r.height = height;
// Remove all rectangles that intersect my rectangle
for (int i = 0; i < mySpace.size(); i++) {
Rectangle other = mySpace.get(i);
if (other.intersects(r)) {
removeRect(i);
i--;
}
}
int remainingHeight = rect.height - height;
int remainingWidth = rect.width - width;
if (PLACE_LEFT) {
if (remainingHeight >= myMinHeight) {
if (PLACE_TOP) {
// Split along vertical line x = rect.x + width:
// (rect.x,rect.y)
// +-------------+-------------------------+
// | | |
// | | |
// | | height |
// | | |
// | | |
// +-------------+ B | rect.h
// | width |
// | | |
// | A |
// | | |
// | |
// +---------------------------------------+
// rect.w
mySpace.add(new Rectangle(rect.x, rect.y + height, width, remainingHeight)); // Area A
} else {
assert PLACE_LEFT;
assert !PLACE_TOP;
// Split along vertical line x = rect.x + width:
// (rect.x,rect.y)
// +-------------+-------------------------+
// | |
// | | |
// | A |
// | | |
// | width |
// +-------------+ B | rect.h
// | | |
// | | |
// | | height |
// | | |
// | | |
// +-------------+-------------------------+
// rect.w
mySpace.add(new Rectangle(rect.x, rect.y, width, remainingHeight)); // Area A
}
}
if (remainingWidth >= myMinWidth) { // Area B
mySpace.add(new Rectangle(rect.x + width, rect.y, remainingWidth, rect.height));
}
}
else {
assert !PLACE_LEFT;
// Split along vertical line x = rect.x + rect.w - width
// (rect.x,rect.y)
// +---------------------------------------+
// | |
// | | |
// | A |
// | | |
// | width |
// | B +--------------| rect.h
// | | |
// | | |
// | height | |
// | | |
// | | |
// +------------------------+--------------+
// rect.w
if (remainingWidth >= myMinWidth) { // Area B
mySpace.add(new Rectangle(rect.x, rect.y, remainingWidth, rect.height));
}
if (remainingHeight >= myMinHeight) { // Area A
if (PLACE_TOP) {
mySpace.add(new Rectangle(rect.x + rect.width - width, rect.y + height, width, remainingHeight));
} else {
mySpace.add(new Rectangle(rect.x + rect.width - width, rect.y, width, remainingHeight));
}
}
}
if (PLACE_LEFT) {
if (PLACE_TOP) {
// Split along horizontal line y = rect.y + height:
// +-------------+-------------------------+
// | | |
// | | height |
// | | A |
// | | |
// | | | rect.h
// +-------------+ - - - - - - - - - - - - |
// | width |
// | |
// | B |
// | |
// | |
// +---------------------------------------+
// rect.w
if (remainingHeight >= myMinHeight) { // Area B
mySpace.add(new Rectangle(rect.x, rect.y + height, rect.width, remainingHeight));
}
if (remainingWidth >= myMinWidth) { // Area A
mySpace.add(new Rectangle(rect.x + width, rect.y, remainingWidth, height));
}
} else {
assert PLACE_LEFT;
assert !PLACE_TOP;
// +---------------------------------------+
// | |
// | |
// | B |
// | |
// | | rect.h
// +-------------+ - - - - - - - - - - - - |
// | width | |
// | | |
// | | height A |
// | | |
// | | |
// +-------------+-------------------------+
// rect.w
if (remainingHeight >= myMinHeight) { // Area B
mySpace.add(new Rectangle(rect.x, rect.y, rect.width, remainingHeight));
}
if (remainingWidth >= myMinWidth) { // Area A
mySpace.add(new Rectangle(rect.x + width, rect.y + rect.height - height, remainingWidth, height));
}
}
}
else {
assert !PLACE_LEFT;
if (PLACE_TOP) {
// Split along horizontal line y = rect.y + rect.h - height:
// +-------------------------+-------------+
// | | |
// | | |
// | A height | |
// | | |
// | | | rect.h
// | - - - - - - - - - - - - +-------------|
// | width |
// | |
// | B |
// | |
// | |
// +---------------------------------------+
// rect.w
if (remainingHeight >= myMinHeight) { // Area B
mySpace.add(new Rectangle(rect.x, rect.y + height, rect.width, remainingHeight));
}
if (remainingWidth >= myMinWidth) { // Area A
mySpace.add(new Rectangle(rect.x, rect.y, remainingWidth, height));
}
} else {
assert !PLACE_LEFT;
assert !PLACE_TOP;
// Split along horizontal line y = rect.y + rect.h - height:
// +---------------------------------------+
// | |
// | |
// | B |
// | |
// | width | rect.h
// | - - - - - - - - - - - - +-------------|
// | | |
// | | |
// | A height | |
// | | |
// | | |
// +-------------------------+-------------+
// rect.w
if (remainingHeight >= myMinHeight) { // Area B
mySpace.add(new Rectangle(rect.x, rect.y, rect.width, remainingHeight));
}
if (remainingWidth >= myMinWidth) { // Area A
mySpace.add(new Rectangle(rect.x, rect.y + rect.height - height, remainingWidth, height));
}
}
}
if (DEBUG) {
myAllocated.add(r);
}
if (DEBUG) {
dumpDiagnostics();
}
// Remove redundant rectangles. This is not very efficient.
for (int i = 0; i < mySpace.size() - 1; i++) {
for (int j = i + 1; j < mySpace.size(); j++) {
Rectangle iRect = mySpace.get(i);
Rectangle jRect = mySpace.get(j);
if (jRect.contains(iRect)) {
removeRect(i);
i--;
break;
}
if (iRect.contains(jRect)) {
removeRect(j);
j--;
}
}
}
if (DEBUG) {
dumpDiagnostics();
}
return r;
}
// DEBUGGING CODE: Enable with DEBUG
private List<Rectangle> myAllocated;
private int myIteration;
@SuppressWarnings("EmptyCatchBlock")
void dumpDiagnostics() {
if (DEBUG) {
myIteration++;
// Determine size
Rectangle r = new Rectangle(0, 0, 100, 100);
for (Rectangle rect : mySpace) {
r.add(rect);
}
BufferedImage image = UIUtil.createImage(r.width, r.height, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.getGraphics();
// Fill background
g.setColor(Color.BLACK);
g.fillRect(0, 0, r.width, r.height);
paintDiagnostics((Graphics2D)g);
g.dispose();
try {
File file = new File("/tmp/layout-iteration-" + myIteration + ".png");
ImageIO.write(image, "PNG", file);
} catch (Exception e) {
}
}
}
@SuppressWarnings({"AssignmentToStaticFieldFromInstanceMethod", "CallToPrintStackTrace", "UseOfSystemOutOrSystemErr"})
void paintDiagnostics(Graphics2D g) {
if (DEBUG) {
Composite prevComposite = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
Color[] colors =
new Color[]{Color.blue, Color.cyan, Color.green, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow,
Color.darkGray, Color.lightGray, Color.gray,};
if (myAllocated != null && !myAllocated.isEmpty()) {
char allocated = 'A';
for (Rectangle rect : myAllocated) {
Color color = new Color(0x9FFFFFFF, true);
g.setColor(color);
g.setBackground(color);
g.fillRect(rect.x, rect.y, rect.width, rect.height);
g.setColor(Color.WHITE);
g.drawRect(rect.x, rect.y, rect.width, rect.height);
g.drawString(String.valueOf(allocated++), rect.x + rect.width / 2, rect.y + rect.height / 2);
}
}
int colorIndex = 0;
for (Rectangle rect : mySpace) {
Color color = colors[colorIndex];
colorIndex = (colorIndex + 1) % colors.length;
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), 128);
g.setColor(color);
g.fillRect(rect.x, rect.y, rect.width, rect.height);
g.setColor(Color.WHITE);
g.drawString(Integer.toString(colorIndex), rect.x + rect.width / 2, rect.y + rect.height / 2);
}
g.setComposite(prevComposite);
}
}
}