| /* |
| * 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++; |
| } |
| } |
| } |