blob: 9fc2e09373c8d1b4e28650d095517f320077a433 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.ide.eclipse.adt.internal.editors.layout.gle2;
import com.android.annotations.Nullable;
import com.android.ide.common.api.Rect;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
/**
* 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.
*/
class BinPacker {
/**
* When enabled, the successive passes are dumped as PNG images showing the
* various available and occupied rectangles)
*/
private static final boolean DEBUG = false;
private final List<Rect> mSpace = new ArrayList<Rect>();
private final int mMinHeight;
private final int mMinWidth;
/**
* Creates a new {@linkplain BinPacker}. To use it, first add one or more
* initial available space rectangles with {@link #addSpace(Rect)}, and then
* place the rectangles with {@link #occupy(int, int)}. The returned
* {@link Rect} 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) {
mMinWidth = minWidth;
mMinHeight = minHeight;
if (DEBUG) {
mAllocated = new ArrayList<Rect>();
sLayoutId++;
sRectId = 1;
}
}
/** Adds more available space */
void addSpace(Rect rect) {
if (rect.w >= mMinWidth && rect.h >= mMinHeight) {
mSpace.add(rect);
}
}
/** Attempts to place a rectangle of the given dimensions, if possible */
@Nullable
Rect 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 (mSpace.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 = mSpace.size(); i < n; i++) {
Rect rect = mSpace.get(i);
if (rect.y <= minY) {
if (rect.w >= width && rect.h >= 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 = mSpace.size(); i < n; i++) {
Rect rect = mSpace.get(i);
if (rect.y != minY) { // Only comparing elements at same y
continue;
}
if (rect.w >= width && rect.h >= height) {
if (width < height) {
int distance = rect.w - width;
if (distance < bestShortDistance ||
distance == bestShortDistance &&
(rect.h - height) < bestLongDistance) {
bestShortDistance = distance;
bestLongDistance = rect.h - height;
bestIndex = i;
}
} else {
int distance = rect.w - width;
if (distance < bestShortDistance ||
distance == bestShortDistance &&
(rect.h - height) < bestLongDistance) {
bestShortDistance = distance;
bestLongDistance = rect.h - 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 #mSpace} list to remove a rectangle
* from
*/
private void removeRect(int index) {
assert !mSpace.isEmpty();
int lastIndex = mSpace.size() - 1;
if (index != lastIndex) {
// Swap before remove to make deletion faster since we don't
// care about order
Rect temp = mSpace.get(index);
mSpace.set(index, mSpace.get(lastIndex));
mSpace.set(lastIndex, temp);
}
mSpace.remove(lastIndex);
}
/**
* Splits the rectangle at the given rectangle index such that it can contain
* a rectangle of the given width and height. */
private Rect split(int index, int width, int height) {
Rect rect = mSpace.get(index);
assert rect.w >= width && rect.h >= height : rect;
Rect r = new Rect(rect);
r.w = width;
r.h = height;
// Remove all rectangles that intersect my rectangle
for (int i = 0; i < mSpace.size(); i++) {
Rect other = mSpace.get(i);
if (other.intersects(r)) {
removeRect(i);
i--;
}
}
// Split along vertical line x = rect.x + width:
// (rect.x,rect.y)
// +-------------+-------------------------+
// | | |
// | | |
// | | height |
// | | |
// | | |
// +-------------+ B | rect.h
// | width |
// | | |
// | A |
// | | |
// | |
// +---------------------------------------+
// rect.w
int remainingHeight = rect.h - height;
int remainingWidth = rect.w - width;
if (remainingHeight >= mMinHeight) {
mSpace.add(new Rect(rect.x, rect.y + height, width, remainingHeight));
}
if (remainingWidth >= mMinWidth) {
mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, rect.h));
}
// Split along horizontal line y = rect.y + height:
// +-------------+-------------------------+
// | | |
// | | height |
// | | A |
// | | |
// | | | rect.h
// +-------------+ - - - - - - - - - - - - |
// | width |
// | |
// | B |
// | |
// | |
// +---------------------------------------+
// rect.w
if (remainingHeight >= mMinHeight) {
mSpace.add(new Rect(rect.x, rect.y + height, rect.w, remainingHeight));
}
if (remainingWidth >= mMinWidth) {
mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, height));
}
// Remove redundant rectangles. This is not very efficient.
for (int i = 0; i < mSpace.size() - 1; i++) {
for (int j = i + 1; j < mSpace.size(); j++) {
Rect iRect = mSpace.get(i);
Rect jRect = mSpace.get(j);
if (jRect.contains(iRect)) {
removeRect(i);
i--;
break;
}
if (iRect.contains(jRect)) {
removeRect(j);
j--;
}
}
}
if (DEBUG) {
mAllocated.add(r);
dumpImage();
}
return r;
}
// DEBUGGING CODE: Enable with DEBUG
private List<Rect> mAllocated;
private static int sLayoutId;
private static int sRectId;
private void dumpImage() {
if (DEBUG) {
int width = 100;
int height = 100;
for (Rect rect : mSpace) {
width = Math.max(width, rect.w);
height = Math.max(height, rect.h);
}
width += 10;
height += 10;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.BLACK);
g.fillRect(0, 0, image.getWidth(), image.getHeight());
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,
};
char allocated = 'A';
for (Rect rect : mAllocated) {
Color color = new Color(0x9FFFFFFF, true);
g.setColor(color);
g.setBackground(color);
g.fillRect(rect.x, rect.y, rect.w, rect.h);
g.setColor(Color.WHITE);
g.drawRect(rect.x, rect.y, rect.w, rect.h);
g.drawString("" + (allocated++),
rect.x + rect.w / 2, rect.y + rect.h / 2);
}
int colorIndex = 0;
for (Rect rect : mSpace) {
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.w, rect.h);
g.setColor(Color.WHITE);
g.drawString(Integer.toString(colorIndex),
rect.x + rect.w / 2, rect.y + rect.h / 2);
}
g.dispose();
File file = new File("/tmp/layout" + sLayoutId + "_pass" + sRectId + ".png");
try {
ImageIO.write(image, "PNG", file);
System.out.println("Wrote diagnostics image " + file);
} catch (IOException e) {
e.printStackTrace();
}
sRectId++;
}
}
}