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 com.android.camera.settings;
18
19import com.android.camera.util.ApiHelper;
20import com.android.ex.camera2.portability.Size;
21
22import java.math.BigInteger;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collections;
26import java.util.Comparator;
27import java.util.HashMap;
28import java.util.LinkedList;
29import java.util.List;
30
31/**
32 * This class is used to help manage the many different resolutions available on
33 * the device. <br/>
34 * It allows you to specify which aspect ratios to offer the user, and then
35 * chooses which resolutions are the most pertinent to avoid overloading the
36 * user with so many options.
37 */
38public class ResolutionUtil {
39
40    public static final String NEXUS_5_LARGE_16_BY_9 = "1836x3264";
41    public static final float NEXUS_5_LARGE_16_BY_9_ASPECT_RATIO = 16f / 9f;
42    public static Size NEXUS_5_LARGE_16_BY_9_SIZE = new Size(1836, 3264);
43
44    /**
45     * These are the preferred aspect ratios for the settings. We will take HAL
46     * supported aspect ratios that are within RATIO_TOLERANCE of these values.
47     * We will also take the maximum supported resolution for full sensor image.
48     */
49    private static Float[] sDesiredAspectRatios = {
50            16.0f / 9.0f, 4.0f / 3.0f
51    };
52
53    private static Size[] sDesiredAspectRatioSizes = {
54            new Size(16, 9), new Size(4, 3)
55    };
56
57    private static final float RATIO_TOLERANCE = .05f;
58
59    /**
60     * A resolution bucket holds a list of sizes that are of a given aspect
61     * ratio.
62     */
63    private static class ResolutionBucket {
64        public Float aspectRatio;
65        /**
66         * This is a sorted list of sizes, going from largest to smallest.
67         */
68        public List<Size> sizes = new LinkedList<Size>();
69        /**
70         * This is the head of the sizes array.
71         */
72        public Size largest;
73        /**
74         * This is the area of the largest size, used for sorting
75         * ResolutionBuckets.
76         */
77        public Integer maxPixels = 0;
78
79        /**
80         * Use this to add a new resolution to this bucket. It will insert it
81         * into the sizes array and update appropriate members.
82         *
83         * @param size the new size to be added
84         */
85        public void add(Size size) {
86            sizes.add(size);
87            Collections.sort(sizes, new Comparator<Size>() {
88                @Override
89                public int compare(Size size, Size size2) {
90                    // sort area greatest to least
91                    return Integer.compare(size2.width() * size2.height(),
92                            size.width() * size.height());
93                }
94            });
95            maxPixels = sizes.get(0).width() * sizes.get(0).height();
96        }
97    }
98
99    /**
100     * Given a list of camera sizes, this uses some heuristics to decide which
101     * options to present to a user. It currently returns up to 3 sizes for each
102     * aspect ratio. The aspect ratios returned include the ones in
103     * sDesiredAspectRatios, and the largest full sensor ratio. T his guarantees
104     * that users can use a full-sensor size, as well as any of the preferred
105     * aspect ratios from above;
106     *
107     * @param sizes A super set of all sizes to be displayed
108     * @param isBackCamera true if these are sizes for the back camera
109     * @return The list of sizes to display grouped first by aspect ratio
110     *         (sorted by maximum area), and sorted within aspect ratio by area)
111     */
112    public static List<Size> getDisplayableSizesFromSupported(List<Size> sizes, boolean isBackCamera) {
113        List<ResolutionBucket> buckets = parseAvailableSizes(sizes, isBackCamera);
114
115        List<Float> sortedDesiredAspectRatios = new ArrayList<Float>();
116        // We want to make sure we support the maximum pixel aspect ratio, even
117        // if it doesn't match a desired aspect ratio
118        sortedDesiredAspectRatios.add(buckets.get(0).aspectRatio.floatValue());
119
120        // Now go through the buckets from largest mp to smallest, adding
121        // desired ratios
122        for (ResolutionBucket bucket : buckets) {
123            Float aspectRatio = bucket.aspectRatio;
124            if (Arrays.asList(sDesiredAspectRatios).contains(aspectRatio)
125                    && !sortedDesiredAspectRatios.contains(aspectRatio)) {
126                sortedDesiredAspectRatios.add(aspectRatio);
127            }
128        }
129
130        List<Size> result = new ArrayList<Size>(sizes.size());
131        for (Float targetRatio : sortedDesiredAspectRatios) {
132            for (ResolutionBucket bucket : buckets) {
133                Number aspectRatio = bucket.aspectRatio;
134                if (Math.abs(aspectRatio.floatValue() - targetRatio) <= RATIO_TOLERANCE) {
135                    result.addAll(pickUpToThree(bucket.sizes));
136                }
137            }
138        }
139        return result;
140    }
141
142    /**
143     * Get the area in pixels of a size.
144     *
145     * @param size the size to measure
146     * @return the area.
147     */
148    private static int area(Size size) {
149        if (size == null) {
150            return 0;
151        }
152        return size.width() * size.height();
153    }
154
155    /**
156     * Given a list of sizes of a similar aspect ratio, it tries to pick evenly
157     * spaced out options. It starts with the largest, then tries to find one at
158     * 50% of the last chosen size for the subsequent size.
159     *
160     * @param sizes A list of Sizes that are all of a similar aspect ratio
161     * @return A list of at least one, and no more than three representative
162     *         sizes from the list.
163     */
164    private static List<Size> pickUpToThree(List<Size> sizes) {
165        List<Size> result = new ArrayList<Size>();
166        Size largest = sizes.get(0);
167        result.add(largest);
168        Size lastSize = largest;
169        for (Size size : sizes) {
170            double targetArea = Math.pow(.5, result.size()) * area(largest);
171            if (area(size) < targetArea) {
172                // This candidate is smaller than half the mega pixels of the
173                // last one. Let's see whether the previous size, or this size
174                // is closer to the desired target.
175                if (!result.contains(lastSize)
176                        && (targetArea - area(lastSize) < area(size) - targetArea)) {
177                    result.add(lastSize);
178                } else {
179                    result.add(size);
180                }
181            }
182            lastSize = size;
183            if (result.size() == 3) {
184                break;
185            }
186        }
187
188        // If we have less than three, we can add the smallest size.
189        if (result.size() < 3 && !result.contains(lastSize)) {
190            result.add(lastSize);
191        }
192        return result;
193    }
194
195    /**
196     * Take an aspect ratio and squish it into a nearby desired aspect ratio, if
197     * possible.
198     *
199     * @param aspectRatio the aspect ratio to fuzz
200     * @return the closest desiredAspectRatio within RATIO_TOLERANCE, or the
201     *         original ratio
202     */
203    private static float fuzzAspectRatio(float aspectRatio) {
204        for (float desiredAspectRatio : sDesiredAspectRatios) {
205            if ((Math.abs(aspectRatio - desiredAspectRatio)) < RATIO_TOLERANCE) {
206                return desiredAspectRatio;
207            }
208        }
209        return aspectRatio;
210    }
211
212    /**
213     * This takes a bunch of supported sizes and buckets them by aspect ratio.
214     * The result is a list of buckets sorted by each bucket's largest area.
215     * They are sorted from largest to smallest. This will bucket aspect ratios
216     * that are close to the sDesiredAspectRatios in to the same bucket.
217     *
218     * @param sizes all supported sizes for a camera
219     * @param isBackCamera true if these are sizes for the back camera
220     * @return all of the sizes grouped by their closest aspect ratio
221     */
222    private static List<ResolutionBucket> parseAvailableSizes(List<Size> sizes, boolean isBackCamera) {
223        HashMap<Float, ResolutionBucket> aspectRatioToBuckets = new HashMap<Float, ResolutionBucket>();
224
225        for (Size size : sizes) {
226            Float aspectRatio = size.width() / (float) size.height();
227            // If this aspect ratio is close to a desired Aspect Ratio,
228            // fuzz it so that they are bucketed together
229            aspectRatio = fuzzAspectRatio(aspectRatio);
230            ResolutionBucket bucket = aspectRatioToBuckets.get(aspectRatio);
231            if (bucket == null) {
232                bucket = new ResolutionBucket();
233                bucket.aspectRatio = aspectRatio;
234                aspectRatioToBuckets.put(aspectRatio, bucket);
235            }
236            bucket.add(size);
237        }
238        if (ApiHelper.IS_NEXUS_5 && isBackCamera) {
239            aspectRatioToBuckets.get(16 / 9.0f).add(NEXUS_5_LARGE_16_BY_9_SIZE);
240        }
241        List<ResolutionBucket> sortedBuckets = new ArrayList<ResolutionBucket>(
242                aspectRatioToBuckets.values());
243        Collections.sort(sortedBuckets, new Comparator<ResolutionBucket>() {
244            @Override
245            public int compare(ResolutionBucket resolutionBucket, ResolutionBucket resolutionBucket2) {
246                return Integer.compare(resolutionBucket2.maxPixels, resolutionBucket.maxPixels);
247            }
248        });
249        return sortedBuckets;
250    }
251
252    /**
253     * Given a size, return a string describing the aspect ratio by reducing the
254     *
255     * @param size the size to describe
256     * @return a string description of the aspect ratio
257     */
258    public static String aspectRatioDescription(Size size) {
259        Size aspectRatio = reduce(size);
260        return aspectRatio.width() + "x" + aspectRatio.height();
261    }
262
263    /**
264     * Reduce an aspect ratio to its lowest common denominator. The ratio of the
265     * input and output sizes is guaranteed to be the same.
266     *
267     * @param aspectRatio the aspect ratio to reduce
268     * @return The reduced aspect ratio which may equal the original.
269     */
270    public static Size reduce(Size aspectRatio) {
271        BigInteger width = BigInteger.valueOf(aspectRatio.width());
272        BigInteger height = BigInteger.valueOf(aspectRatio.height());
273        BigInteger gcd = width.gcd(height);
274        int numerator = Math.max(width.intValue(), height.intValue()) / gcd.intValue();
275        int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue();
276        return new Size(numerator, denominator);
277    }
278
279    /**
280     * Given a size return the numerator of its aspect ratio
281     *
282     * @param size the size to measure
283     * @return the numerator
284     */
285    public static int aspectRatioNumerator(Size size) {
286        Size aspectRatio = reduce(size);
287        return aspectRatio.width();
288    }
289
290    /**
291     * Given a size, return the closest aspect ratio that falls close to the
292     * given size.
293     *
294     * @param size the size to approximate
295     * @return the closest desired aspect ratio, or the original aspect ratio if
296     *         none were close enough
297     */
298    public static Size getApproximateSize(Size size) {
299        Size aspectRatio = reduce(size);
300        float fuzzy = fuzzAspectRatio(size.width() / (float) size.height());
301        int index = Arrays.asList(sDesiredAspectRatios).indexOf(fuzzy);
302        if (index != -1) {
303            aspectRatio = new Size(sDesiredAspectRatioSizes[index]);
304        }
305        return aspectRatio;
306    }
307
308    /**
309     * See {@link #getApproximateSize(Size)}.
310     * <p>
311     * TODO: Move this whole util to {@link android.util.Size}
312     */
313    public static com.android.camera.util.Size getApproximateSize(
314            com.android.camera.util.Size size) {
315        Size result = getApproximateSize(new Size(size.getWidth(), size.getHeight()));
316        return new com.android.camera.util.Size(result.width(), result.height());
317    }
318
319    /**
320     * Given a size return the numerator of its aspect ratio
321     *
322     * @param size
323     * @return the denominator
324     */
325    public static int aspectRatioDenominator(Size size) {
326        BigInteger width = BigInteger.valueOf(size.width());
327        BigInteger height = BigInteger.valueOf(size.height());
328        BigInteger gcd = width.gcd(height);
329        int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue();
330        return denominator;
331    }
332
333}
334