1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 17 18 import com.android.annotations.Nullable; 19 import com.android.ide.common.api.Rect; 20 21 import java.awt.Color; 22 import java.awt.Graphics2D; 23 import java.awt.image.BufferedImage; 24 import java.io.File; 25 import java.io.IOException; 26 import java.util.ArrayList; 27 import java.util.List; 28 29 import javax.imageio.ImageIO; 30 31 /** 32 * This class implements 2D bin packing: packing rectangles into a given area as 33 * tightly as "possible" (bin packing in general is NP hard, so this class uses 34 * heuristics). 35 * <p> 36 * The algorithm implemented is to keep a set of (possibly overlapping) 37 * available areas for placement. For each newly inserted rectangle, we first 38 * pick which available space to occupy, and we then subdivide the 39 * current rectangle into all the possible remaining unoccupied sub-rectangles. 40 * We also remove any other space rectangles which are no longer eligible if 41 * they are intersecting the newly placed rectangle. 42 * <p> 43 * This algorithm is not very fast, so should not be used for a large number of 44 * rectangles. 45 */ 46 class BinPacker { 47 /** 48 * When enabled, the successive passes are dumped as PNG images showing the 49 * various available and occupied rectangles) 50 */ 51 private static final boolean DEBUG = false; 52 53 private final List<Rect> mSpace = new ArrayList<Rect>(); 54 private final int mMinHeight; 55 private final int mMinWidth; 56 57 /** 58 * Creates a new {@linkplain BinPacker}. To use it, first add one or more 59 * initial available space rectangles with {@link #addSpace(Rect)}, and then 60 * place the rectangles with {@link #occupy(int, int)}. The returned 61 * {@link Rect} from {@link #occupy(int, int)} gives the coordinates of the 62 * positioned rectangle. 63 * 64 * @param minWidth the smallest width of any rectangle placed into this bin 65 * @param minHeight the smallest height of any rectangle placed into this bin 66 */ BinPacker(int minWidth, int minHeight)67 BinPacker(int minWidth, int minHeight) { 68 mMinWidth = minWidth; 69 mMinHeight = minHeight; 70 71 if (DEBUG) { 72 mAllocated = new ArrayList<Rect>(); 73 sLayoutId++; 74 sRectId = 1; 75 } 76 } 77 78 /** Adds more available space */ addSpace(Rect rect)79 void addSpace(Rect rect) { 80 if (rect.w >= mMinWidth && rect.h >= mMinHeight) { 81 mSpace.add(rect); 82 } 83 } 84 85 /** Attempts to place a rectangle of the given dimensions, if possible */ 86 @Nullable occupy(int width, int height)87 Rect occupy(int width, int height) { 88 int index = findBest(width, height); 89 if (index == -1) { 90 return null; 91 } 92 93 return split(index, width, height); 94 } 95 96 /** 97 * Finds the best available space rectangle to position a new rectangle of 98 * the given size in. 99 */ findBest(int width, int height)100 private int findBest(int width, int height) { 101 if (mSpace.isEmpty()) { 102 return -1; 103 } 104 105 // Try to pack as far up as possible first 106 int bestIndex = -1; 107 boolean multipleAtSameY = false; 108 int minY = Integer.MAX_VALUE; 109 for (int i = 0, n = mSpace.size(); i < n; i++) { 110 Rect rect = mSpace.get(i); 111 if (rect.y <= minY) { 112 if (rect.w >= width && rect.h >= height) { 113 if (rect.y < minY) { 114 minY = rect.y; 115 multipleAtSameY = false; 116 bestIndex = i; 117 } else if (minY == rect.y) { 118 multipleAtSameY = true; 119 } 120 } 121 } 122 } 123 124 if (!multipleAtSameY) { 125 return bestIndex; 126 } 127 128 bestIndex = -1; 129 130 // Pick a rectangle. This currently tries to find the rectangle whose shortest 131 // side most closely matches the placed rectangle's size. 132 // Attempt to find the best short side fit 133 int bestShortDistance = Integer.MAX_VALUE; 134 int bestLongDistance = Integer.MAX_VALUE; 135 136 for (int i = 0, n = mSpace.size(); i < n; i++) { 137 Rect rect = mSpace.get(i); 138 if (rect.y != minY) { // Only comparing elements at same y 139 continue; 140 } 141 if (rect.w >= width && rect.h >= height) { 142 if (width < height) { 143 int distance = rect.w - width; 144 if (distance < bestShortDistance || 145 distance == bestShortDistance && 146 (rect.h - height) < bestLongDistance) { 147 bestShortDistance = distance; 148 bestLongDistance = rect.h - height; 149 bestIndex = i; 150 } 151 } else { 152 int distance = rect.w - width; 153 if (distance < bestShortDistance || 154 distance == bestShortDistance && 155 (rect.h - height) < bestLongDistance) { 156 bestShortDistance = distance; 157 bestLongDistance = rect.h - height; 158 bestIndex = i; 159 } 160 } 161 } 162 } 163 164 return bestIndex; 165 } 166 167 /** 168 * Removes the rectangle at the given index. Since the rectangles are in an 169 * {@link ArrayList}, removing a rectangle in the normal way is slow (it 170 * would involve shifting all elements), but since we don't care about 171 * order, this always swaps the to-be-deleted element to the last position 172 * in the array first, <b>then</b> it deletes it (which should be 173 * immediate). 174 * 175 * @param index the index in the {@link #mSpace} list to remove a rectangle 176 * from 177 */ removeRect(int index)178 private void removeRect(int index) { 179 assert !mSpace.isEmpty(); 180 int lastIndex = mSpace.size() - 1; 181 if (index != lastIndex) { 182 // Swap before remove to make deletion faster since we don't 183 // care about order 184 Rect temp = mSpace.get(index); 185 mSpace.set(index, mSpace.get(lastIndex)); 186 mSpace.set(lastIndex, temp); 187 } 188 189 mSpace.remove(lastIndex); 190 } 191 192 /** 193 * Splits the rectangle at the given rectangle index such that it can contain 194 * a rectangle of the given width and height. */ split(int index, int width, int height)195 private Rect split(int index, int width, int height) { 196 Rect rect = mSpace.get(index); 197 assert rect.w >= width && rect.h >= height : rect; 198 199 Rect r = new Rect(rect); 200 r.w = width; 201 r.h = height; 202 203 // Remove all rectangles that intersect my rectangle 204 for (int i = 0; i < mSpace.size(); i++) { 205 Rect other = mSpace.get(i); 206 if (other.intersects(r)) { 207 removeRect(i); 208 i--; 209 } 210 } 211 212 213 // Split along vertical line x = rect.x + width: 214 // (rect.x,rect.y) 215 // +-------------+-------------------------+ 216 // | | | 217 // | | | 218 // | | height | 219 // | | | 220 // | | | 221 // +-------------+ B | rect.h 222 // | width | 223 // | | | 224 // | A | 225 // | | | 226 // | | 227 // +---------------------------------------+ 228 // rect.w 229 int remainingHeight = rect.h - height; 230 int remainingWidth = rect.w - width; 231 if (remainingHeight >= mMinHeight) { 232 mSpace.add(new Rect(rect.x, rect.y + height, width, remainingHeight)); 233 } 234 if (remainingWidth >= mMinWidth) { 235 mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, rect.h)); 236 } 237 238 // Split along horizontal line y = rect.y + height: 239 // +-------------+-------------------------+ 240 // | | | 241 // | | height | 242 // | | A | 243 // | | | 244 // | | | rect.h 245 // +-------------+ - - - - - - - - - - - - | 246 // | width | 247 // | | 248 // | B | 249 // | | 250 // | | 251 // +---------------------------------------+ 252 // rect.w 253 if (remainingHeight >= mMinHeight) { 254 mSpace.add(new Rect(rect.x, rect.y + height, rect.w, remainingHeight)); 255 } 256 if (remainingWidth >= mMinWidth) { 257 mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, height)); 258 } 259 260 // Remove redundant rectangles. This is not very efficient. 261 for (int i = 0; i < mSpace.size() - 1; i++) { 262 for (int j = i + 1; j < mSpace.size(); j++) { 263 Rect iRect = mSpace.get(i); 264 Rect jRect = mSpace.get(j); 265 if (jRect.contains(iRect)) { 266 removeRect(i); 267 i--; 268 break; 269 } 270 if (iRect.contains(jRect)) { 271 removeRect(j); 272 j--; 273 } 274 } 275 } 276 277 if (DEBUG) { 278 mAllocated.add(r); 279 dumpImage(); 280 } 281 282 return r; 283 } 284 285 // DEBUGGING CODE: Enable with DEBUG 286 287 private List<Rect> mAllocated; 288 private static int sLayoutId; 289 private static int sRectId; dumpImage()290 private void dumpImage() { 291 if (DEBUG) { 292 int width = 100; 293 int height = 100; 294 for (Rect rect : mSpace) { 295 width = Math.max(width, rect.w); 296 height = Math.max(height, rect.h); 297 } 298 width += 10; 299 height += 10; 300 301 BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 302 Graphics2D g = image.createGraphics(); 303 g.setColor(Color.BLACK); 304 g.fillRect(0, 0, image.getWidth(), image.getHeight()); 305 306 Color[] colors = new Color[] { 307 Color.blue, Color.cyan, 308 Color.green, Color.magenta, Color.orange, 309 Color.pink, Color.red, Color.white, Color.yellow, Color.darkGray, 310 Color.lightGray, Color.gray, 311 }; 312 313 char allocated = 'A'; 314 for (Rect rect : mAllocated) { 315 Color color = new Color(0x9FFFFFFF, true); 316 g.setColor(color); 317 g.setBackground(color); 318 g.fillRect(rect.x, rect.y, rect.w, rect.h); 319 g.setColor(Color.WHITE); 320 g.drawRect(rect.x, rect.y, rect.w, rect.h); 321 g.drawString("" + (allocated++), 322 rect.x + rect.w / 2, rect.y + rect.h / 2); 323 } 324 325 int colorIndex = 0; 326 for (Rect rect : mSpace) { 327 Color color = colors[colorIndex]; 328 colorIndex = (colorIndex + 1) % colors.length; 329 330 color = new Color(color.getRed(), color.getGreen(), color.getBlue(), 128); 331 g.setColor(color); 332 333 g.fillRect(rect.x, rect.y, rect.w, rect.h); 334 g.setColor(Color.WHITE); 335 g.drawString(Integer.toString(colorIndex), 336 rect.x + rect.w / 2, rect.y + rect.h / 2); 337 } 338 339 340 g.dispose(); 341 342 File file = new File("/tmp/layout" + sLayoutId + "_pass" + sRectId + ".png"); 343 try { 344 ImageIO.write(image, "PNG", file); 345 System.out.println("Wrote diagnostics image " + file); 346 } catch (IOException e) { 347 e.printStackTrace(); 348 } 349 sRectId++; 350 } 351 } 352 }