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