1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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
17package android.graphics;
18
19/**
20 * @hide
21 */
22public class Atlas {
23    /**
24     * This flag indicates whether the packing algorithm will attempt
25     * to rotate entries to make them fit better in the atlas.
26     */
27    public static final int FLAG_ALLOW_ROTATIONS = 0x1;
28    /**
29     * This flag indicates whether the packing algorithm should leave
30     * an empty 1 pixel wide border around each bitmap. This border can
31     * be useful if the content of the atlas will be used in OpenGL using
32     * bilinear filtering.
33     */
34    public static final int FLAG_ADD_PADDING = 0x2;
35    /**
36     * Default flags: allow rotations and add padding.
37     */
38    public static final int FLAG_DEFAULTS = FLAG_ADD_PADDING;
39
40    /**
41     * Each type defines a different packing algorithm that can
42     * be used by an {@link Atlas}. The best algorithm to use
43     * will depend on the source dataset and the dimensions of
44     * the atlas.
45     */
46    public enum Type {
47        SliceMinArea,
48        SliceMaxArea,
49        SliceShortAxis,
50        SliceLongAxis
51    }
52
53    /**
54     * Represents a bitmap packed in the atlas. Each entry has a location in
55     * pixels in the atlas and a rotation flag. If the entry was rotated, the
56     * bitmap must be rotated by 90 degrees (in either direction as long as
57     * the origin remains the same) before being rendered into the atlas.
58     */
59    public static class Entry {
60        /**
61         * Location, in pixels, of the bitmap on the X axis in the atlas.
62         */
63        public int x;
64        /**
65         * Location, in pixels, of the bitmap on the Y axis in the atlas.
66         */
67        public int y;
68
69        /**
70         * If true, the bitmap must be rotated 90 degrees in the atlas.
71         */
72        public boolean rotated;
73    }
74
75    private final Policy mPolicy;
76
77    /**
78     * Creates a new atlas with the specified algorithm and dimensions
79     * in pixels. Calling this constructor is equivalent to calling
80     * {@link #Atlas(Atlas.Type, int, int, int)} with {@link #FLAG_DEFAULTS}.
81     *
82     * @param type The algorithm to use to pack rectangles in the atlas
83     * @param width The width of the atlas in pixels
84     * @param height The height of the atlas in pixels
85     *
86     * @see #Atlas(Atlas.Type, int, int, int)
87     */
88    public Atlas(Type type, int width, int height) {
89        this(type, width, height, FLAG_DEFAULTS);
90    }
91
92    /**
93     * Creates a new atlas with the specified algorithm and dimensions
94     * in pixels. A set of flags can also be specified to control the
95     * behavior of the atlas.
96     *
97     * @param type The algorithm to use to pack rectangles in the atlas
98     * @param width The width of the atlas in pixels
99     * @param height The height of the atlas in pixels
100     * @param flags Optional flags to control the behavior of the atlas:
101     *              {@link #FLAG_ADD_PADDING}, {@link #FLAG_ALLOW_ROTATIONS}
102     *
103     * @see #Atlas(Atlas.Type, int, int)
104     */
105    public Atlas(Type type, int width, int height, int flags) {
106        mPolicy = findPolicy(type, width, height, flags);
107    }
108
109    /**
110     * Packs a rectangle of the specified dimensions in this atlas.
111     *
112     * @param width The width of the rectangle to pack in the atlas
113     * @param height The height of the rectangle to pack in the atlas
114     *
115     * @return An {@link Entry} instance if the rectangle was packed in
116     *         the atlas, or null if the rectangle could not fit
117     *
118     * @see #pack(int, int, Atlas.Entry)
119     */
120    public Entry pack(int width, int height) {
121        return pack(width, height, null);
122    }
123
124    /**
125     * Packs a rectangle of the specified dimensions in this atlas.
126     *
127     * @param width The width of the rectangle to pack in the atlas
128     * @param height The height of the rectangle to pack in the atlas
129     * @param entry Out parameter that will be filled in with the location
130     *              and attributes of the packed rectangle, can be null
131     *
132     * @return An {@link Entry} instance if the rectangle was packed in
133     *         the atlas, or null if the rectangle could not fit
134     *
135     * @see #pack(int, int)
136     */
137    public Entry pack(int width, int height, Entry entry) {
138        if (entry == null) entry = new Entry();
139        return mPolicy.pack(width, height, entry);
140    }
141
142    private static Policy findPolicy(Type type, int width, int height, int flags) {
143        switch (type) {
144            case SliceMinArea:
145                return new SlicePolicy(width, height, flags,
146                        new SlicePolicy.MinAreaSplitDecision());
147            case SliceMaxArea:
148                return new SlicePolicy(width, height, flags,
149                        new SlicePolicy.MaxAreaSplitDecision());
150            case SliceShortAxis:
151                return new SlicePolicy(width, height, flags,
152                        new SlicePolicy.ShorterFreeAxisSplitDecision());
153            case SliceLongAxis:
154                return new SlicePolicy(width, height, flags,
155                        new SlicePolicy.LongerFreeAxisSplitDecision());
156        }
157        return null;
158    }
159
160    /**
161     * A policy defines how the atlas performs the packing operation.
162     */
163    private static abstract class Policy {
164        abstract Entry pack(int width, int height, Entry entry);
165    }
166
167    /**
168     * The Slice algorightm divides the remaining empty space either
169     * horizontally or vertically after a bitmap is placed in the atlas.
170     *
171     * NOTE: the algorithm is explained below using a tree but is
172     * implemented using a linked list instead for performance reasons.
173     *
174     * The algorithm starts with a single empty cell covering the entire
175     * atlas:
176     *
177     *  -----------------------
178     * |                       |
179     * |                       |
180     * |                       |
181     * |      Empty space      |
182     * |          (C0)         |
183     * |                       |
184     * |                       |
185     * |                       |
186     *  -----------------------
187     *
188     * The tree of cells looks like this:
189     *
190     * N0(free)
191     *
192     * The algorithm then places a bitmap B1, if possible:
193     *
194     *  -----------------------
195     * |        |              |
196     * |   B1   |              |
197     * |        |              |
198     * |--------               |
199     * |                       |
200     * |                       |
201     * |                       |
202     * |                       |
203     *  -----------------------
204     *
205     *  After placing a bitmap in an empty cell, the algorithm splits
206     *  the remaining space in two new empty cells. The split can occur
207     *  vertically or horizontally (this is controlled by the "split
208     *  decision" parameter of the algorithm.)
209     *
210     *  Here is for the instance the result of a vertical split:
211     *
212     *  -----------------------
213     * |        |              |
214     * |   B1   |              |
215     * |        |              |
216     * |--------|      C2      |
217     * |        |              |
218     * |        |              |
219     * |   C1   |              |
220     * |        |              |
221     *  -----------------------
222     *
223     * The cells tree now looks like this:
224     *
225     *       C0(occupied)
226     *           / \
227     *          /   \
228     *         /     \
229     *        /       \
230     *    C1(free)  C2(free)
231     *
232     * For each bitmap to place in the atlas, the Slice algorithm
233     * will visit the free cells until it finds one where a bitmap can
234     * fit. It will then split the now occupied cell and proceed onto
235     * the next bitmap.
236     */
237    private static class SlicePolicy extends Policy {
238        private final Cell mRoot = new Cell();
239
240        private final SplitDecision mSplitDecision;
241
242        private final boolean mAllowRotation;
243        private final int mPadding;
244
245        /**
246         * A cell represents a sub-rectangle of the atlas. A cell is
247         * a node in a linked list representing the available free
248         * space in the atlas.
249         */
250        private static class Cell {
251            int x;
252            int y;
253
254            int width;
255            int height;
256
257            Cell next;
258
259            @Override
260            public String toString() {
261                return String.format("cell[x=%d y=%d width=%d height=%d", x, y, width, height);
262            }
263        }
264
265        SlicePolicy(int width, int height, int flags, SplitDecision splitDecision) {
266            mAllowRotation = (flags & FLAG_ALLOW_ROTATIONS) != 0;
267            mPadding = (flags & FLAG_ADD_PADDING) != 0 ? 1 : 0;
268
269            // The entire atlas is empty at first, minus padding
270            Cell first = new Cell();
271            first.x = first.y = mPadding;
272            first.width = width - 2 * mPadding;
273            first.height = height - 2 * mPadding;
274
275            mRoot.next = first;
276            mSplitDecision = splitDecision;
277        }
278
279        @Override
280        Entry pack(int width, int height, Entry entry) {
281            Cell cell = mRoot.next;
282            Cell prev = mRoot;
283
284            while (cell != null) {
285                if (insert(cell, prev, width, height, entry)) {
286                    return entry;
287                }
288
289                prev = cell;
290                cell = cell.next;
291            }
292
293            return null;
294        }
295
296        /**
297         * Defines how the remaining empty space should be split up:
298         * vertically or horizontally.
299         */
300        private static interface SplitDecision {
301            /**
302             * Returns true if the remaining space defined by
303             * <code>freeWidth</code> and <code>freeHeight</code>
304             * should be split horizontally.
305             *
306             * @param freeWidth The rectWidth of the free space after packing a rectangle
307             * @param freeHeight The rectHeight of the free space after packing a rectangle
308             * @param rectWidth The rectWidth of the rectangle that was packed in a cell
309             * @param rectHeight The rectHeight of the rectangle that was packed in a cell
310             */
311            boolean splitHorizontal(int freeWidth, int freeHeight,
312                    int rectWidth, int rectHeight);
313        }
314
315        // Splits the free area horizontally to minimize the horizontal section area
316        private static class MinAreaSplitDecision implements SplitDecision {
317            @Override
318            public boolean splitHorizontal(int freeWidth, int freeHeight,
319                    int rectWidth, int rectHeight) {
320                return rectWidth * freeHeight > freeWidth * rectHeight;
321            }
322        }
323
324        // Splits the free area horizontally to maximize the horizontal section area
325        private static class MaxAreaSplitDecision implements SplitDecision {
326            @Override
327            public boolean splitHorizontal(int freeWidth, int freeHeight,
328                    int rectWidth, int rectHeight) {
329                return rectWidth * freeHeight <= freeWidth * rectHeight;
330            }
331        }
332
333        // Splits the free area horizontally if the horizontal axis is shorter
334        private static class ShorterFreeAxisSplitDecision implements SplitDecision {
335            @Override
336            public boolean splitHorizontal(int freeWidth, int freeHeight,
337                    int rectWidth, int rectHeight) {
338                return freeWidth <= freeHeight;
339            }
340        }
341
342        // Splits the free area horizontally if the vertical axis is shorter
343        private static class LongerFreeAxisSplitDecision implements SplitDecision {
344            @Override
345            public boolean splitHorizontal(int freeWidth, int freeHeight,
346                    int rectWidth, int rectHeight) {
347                return freeWidth > freeHeight;
348            }
349        }
350
351        /**
352         * Attempts to pack a rectangle of specified dimensions in the available
353         * empty space.
354         *
355         * @param cell The cell representing free space in which to pack the rectangle
356         * @param prev The previous cell in the free space linked list
357         * @param width The width of the rectangle to pack
358         * @param height The height of the rectangle to pack
359         * @param entry Stores the location of the packged rectangle, if it fits
360         *
361         * @return True if the rectangle was packed in the atlas, false otherwise
362         */
363        @SuppressWarnings("SuspiciousNameCombination")
364        private boolean insert(Cell cell, Cell prev, int width, int height, Entry entry) {
365            boolean rotated = false;
366
367            // If the rectangle doesn't fit we'll try to rotate it
368            // if possible before giving up
369            if (cell.width < width || cell.height < height) {
370                if (mAllowRotation) {
371                    if (cell.width < height || cell.height < width) {
372                        return false;
373                    }
374
375                    // Rotate the rectangle
376                    int temp = width;
377                    width = height;
378                    height = temp;
379                    rotated = true;
380                } else {
381                    return false;
382                }
383            }
384
385            // Remaining free space after packing the rectangle
386            int deltaWidth = cell.width - width;
387            int deltaHeight = cell.height - height;
388
389            // Split the remaining free space into two new cells
390            Cell first = new Cell();
391            Cell second = new Cell();
392
393            first.x = cell.x + width + mPadding;
394            first.y = cell.y;
395            first.width = deltaWidth - mPadding;
396
397            second.x = cell.x;
398            second.y = cell.y + height + mPadding;
399            second.height = deltaHeight - mPadding;
400
401            if (mSplitDecision.splitHorizontal(deltaWidth, deltaHeight,
402                    width, height)) {
403                first.height = height;
404                second.width = cell.width;
405            } else {
406                first.height = cell.height;
407                second.width = width;
408
409                // The order of the cells matters for efficient packing
410                // We want to give priority to the cell chosen by the
411                // split decision heuristic
412                Cell temp = first;
413                first = second;
414                second = temp;
415            }
416
417            // Remove degenerate cases to keep the free list as small as possible
418            if (first.width > 0 && first.height > 0) {
419                prev.next = first;
420                prev = first;
421            }
422
423            if (second.width > 0 && second.height > 0) {
424                prev.next = second;
425                second.next = cell.next;
426            } else {
427                prev.next = cell.next;
428            }
429
430            // The cell is now completely removed from the free list
431            cell.next = null;
432
433            // Return the location and rotation of the packed rectangle
434            entry.x = cell.x;
435            entry.y = cell.y;
436            entry.rotated = rotated;
437
438            return true;
439        }
440    }
441}
442