diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java new file mode 100644 index 000000000..9fc2e0937 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java @@ -0,0 +1,352 @@ +/* + * 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++; + } + } +}
\ No newline at end of file |