1/*
2 * Copyright (C) 2012 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 */
16package com.android.camera.crop;
17
18import android.graphics.Matrix;
19import android.graphics.Rect;
20import android.graphics.RectF;
21
22import java.util.Arrays;
23
24/**
25 * Maintains invariant that inner rectangle is constrained to be within the
26 * outer, rotated rectangle.
27 */
28public class BoundedRect {
29    private float rot;
30    private RectF outer;
31    private RectF inner;
32    private float[] innerRotated;
33
34    public BoundedRect(float rotation, Rect outerRect, Rect innerRect) {
35        rot = rotation;
36        outer = new RectF(outerRect);
37        inner = new RectF(innerRect);
38        innerRotated = CropMath.getCornersFromRect(inner);
39        rotateInner();
40        if (!isConstrained())
41            reconstrain();
42    }
43
44    public BoundedRect(float rotation, RectF outerRect, RectF innerRect) {
45        rot = rotation;
46        outer = new RectF(outerRect);
47        inner = new RectF(innerRect);
48        innerRotated = CropMath.getCornersFromRect(inner);
49        rotateInner();
50        if (!isConstrained())
51            reconstrain();
52    }
53
54    public void resetTo(float rotation, RectF outerRect, RectF innerRect) {
55        rot = rotation;
56        outer.set(outerRect);
57        inner.set(innerRect);
58        innerRotated = CropMath.getCornersFromRect(inner);
59        rotateInner();
60        if (!isConstrained())
61            reconstrain();
62    }
63
64    /**
65     * Sets inner, and re-constrains it to fit within the rotated bounding rect.
66     */
67    public void setInner(RectF newInner) {
68        if (inner.equals(newInner))
69            return;
70        inner = newInner;
71        innerRotated = CropMath.getCornersFromRect(inner);
72        rotateInner();
73        if (!isConstrained())
74            reconstrain();
75    }
76
77    /**
78     * Sets rotation, and re-constrains inner to fit within the rotated bounding rect.
79     */
80    public void setRotation(float rotation) {
81        if (rotation == rot)
82            return;
83        rot = rotation;
84        innerRotated = CropMath.getCornersFromRect(inner);
85        rotateInner();
86        if (!isConstrained())
87            reconstrain();
88    }
89
90    public void setToInner(RectF r) {
91        r.set(inner);
92    }
93
94    public void setToOuter(RectF r) {
95        r.set(outer);
96    }
97
98    public RectF getInner() {
99        return new RectF(inner);
100    }
101
102    public RectF getOuter() {
103        return new RectF(outer);
104    }
105
106    /**
107     * Tries to move the inner rectangle by (dx, dy).  If this would cause it to leave
108     * the bounding rectangle, snaps the inner rectangle to the edge of the bounding
109     * rectangle.
110     */
111    public void moveInner(float dx, float dy) {
112        Matrix m0 = getInverseRotMatrix();
113
114        RectF translatedInner = new RectF(inner);
115        translatedInner.offset(dx, dy);
116
117        float[] translatedInnerCorners = CropMath.getCornersFromRect(translatedInner);
118        float[] outerCorners = CropMath.getCornersFromRect(outer);
119
120        m0.mapPoints(translatedInnerCorners);
121        float[] correction = {
122                0, 0
123        };
124
125        // find correction vectors for corners that have moved out of bounds
126        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
127            float correctedInnerX = translatedInnerCorners[i] + correction[0];
128            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
129            if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
130                float[] badCorner = {
131                        correctedInnerX, correctedInnerY
132                };
133                float[] nearestSide = CropMath.closestSide(badCorner, outerCorners);
134                float[] correctionVec =
135                        GeometryMathUtils.shortestVectorFromPointToLine(badCorner, nearestSide);
136                correction[0] += correctionVec[0];
137                correction[1] += correctionVec[1];
138            }
139        }
140
141        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
142            float correctedInnerX = translatedInnerCorners[i] + correction[0];
143            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
144            if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
145                float[] correctionVec = {
146                        correctedInnerX, correctedInnerY
147                };
148                CropMath.getEdgePoints(outer, correctionVec);
149                correctionVec[0] -= correctedInnerX;
150                correctionVec[1] -= correctedInnerY;
151                correction[0] += correctionVec[0];
152                correction[1] += correctionVec[1];
153            }
154        }
155
156        // Set correction
157        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
158            float correctedInnerX = translatedInnerCorners[i] + correction[0];
159            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
160            // update translated corners with correction vectors
161            translatedInnerCorners[i] = correctedInnerX;
162            translatedInnerCorners[i + 1] = correctedInnerY;
163        }
164
165        innerRotated = translatedInnerCorners;
166        // reconstrain to update inner
167        reconstrain();
168    }
169
170    /**
171     * Attempts to resize the inner rectangle.  If this would cause it to leave
172     * the bounding rect, clips the inner rectangle to fit.
173     */
174    public void resizeInner(RectF newInner) {
175        Matrix m = getRotMatrix();
176        Matrix m0 = getInverseRotMatrix();
177
178        float[] outerCorners = CropMath.getCornersFromRect(outer);
179        m.mapPoints(outerCorners);
180        float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
181        float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
182        RectF ret = new RectF(newInner);
183
184        for (int i = 0; i < newInnerCorners.length; i += 2) {
185            float[] c = {
186                    newInnerCorners[i], newInnerCorners[i + 1]
187            };
188            float[] c0 = Arrays.copyOf(c, 2);
189            m0.mapPoints(c0);
190            if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
191                float[] outerSide = CropMath.closestSide(c, outerCorners);
192                float[] pathOfCorner = {
193                        newInnerCorners[i], newInnerCorners[i + 1],
194                        oldInnerCorners[i], oldInnerCorners[i + 1]
195                };
196                float[] p = GeometryMathUtils.lineIntersect(pathOfCorner, outerSide);
197                if (p == null) {
198                    // lines are parallel or not well defined, so don't resize
199                    p = new float[2];
200                    p[0] = oldInnerCorners[i];
201                    p[1] = oldInnerCorners[i + 1];
202                }
203                // relies on corners being in same order as method
204                // getCornersFromRect
205                switch (i) {
206                    case 0:
207                    case 1:
208                        ret.left = (p[0] > ret.left) ? p[0] : ret.left;
209                        ret.top = (p[1] > ret.top) ? p[1] : ret.top;
210                        break;
211                    case 2:
212                    case 3:
213                        ret.right = (p[0] < ret.right) ? p[0] : ret.right;
214                        ret.top = (p[1] > ret.top) ? p[1] : ret.top;
215                        break;
216                    case 4:
217                    case 5:
218                        ret.right = (p[0] < ret.right) ? p[0] : ret.right;
219                        ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
220                        break;
221                    case 6:
222                    case 7:
223                        ret.left = (p[0] > ret.left) ? p[0] : ret.left;
224                        ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
225                        break;
226                    default:
227                        break;
228                }
229            }
230        }
231        float[] retCorners = CropMath.getCornersFromRect(ret);
232        m0.mapPoints(retCorners);
233        innerRotated = retCorners;
234        // reconstrain to update inner
235        reconstrain();
236    }
237
238    /**
239     * Attempts to resize the inner rectangle.  If this would cause it to leave
240     * the bounding rect, clips the inner rectangle to fit while maintaining
241     * aspect ratio.
242     */
243    public void fixedAspectResizeInner(RectF newInner) {
244        Matrix m = getRotMatrix();
245        Matrix m0 = getInverseRotMatrix();
246
247        float aspectW = inner.width();
248        float aspectH = inner.height();
249        float aspRatio = aspectW / aspectH;
250        float[] corners = CropMath.getCornersFromRect(outer);
251
252        m.mapPoints(corners);
253        float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
254        float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
255
256        // find fixed corner
257        int fixed = -1;
258        if (inner.top == newInner.top) {
259            if (inner.left == newInner.left)
260                fixed = 0; // top left
261            else if (inner.right == newInner.right)
262                fixed = 2; // top right
263        } else if (inner.bottom == newInner.bottom) {
264            if (inner.right == newInner.right)
265                fixed = 4; // bottom right
266            else if (inner.left == newInner.left)
267                fixed = 6; // bottom left
268        }
269        // no fixed corner, return without update
270        if (fixed == -1)
271            return;
272        float widthSoFar = newInner.width();
273        int moved = -1;
274        for (int i = 0; i < newInnerCorners.length; i += 2) {
275            float[] c = {
276                    newInnerCorners[i], newInnerCorners[i + 1]
277            };
278            float[] c0 = Arrays.copyOf(c, 2);
279            m0.mapPoints(c0);
280            if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
281                moved = i;
282                if (moved == fixed)
283                    continue;
284                float[] l2 = CropMath.closestSide(c, corners);
285                float[] l1 = {
286                        newInnerCorners[i], newInnerCorners[i + 1],
287                        oldInnerCorners[i], oldInnerCorners[i + 1]
288                };
289                float[] p = GeometryMathUtils.lineIntersect(l1, l2);
290                if (p == null) {
291                    // lines are parallel or not well defined, so set to old
292                    // corner
293                    p = new float[2];
294                    p[0] = oldInnerCorners[i];
295                    p[1] = oldInnerCorners[i + 1];
296                }
297                // relies on corners being in same order as method
298                // getCornersFromRect
299                float fixed_x = oldInnerCorners[fixed];
300                float fixed_y = oldInnerCorners[fixed + 1];
301                float newWidth = Math.abs(fixed_x - p[0]);
302                float newHeight = Math.abs(fixed_y - p[1]);
303                newWidth = Math.max(newWidth, aspRatio * newHeight);
304                if (newWidth < widthSoFar)
305                    widthSoFar = newWidth;
306            }
307        }
308
309        float heightSoFar = widthSoFar / aspRatio;
310        RectF ret = new RectF(inner);
311        if (fixed == 0) {
312            ret.right = ret.left + widthSoFar;
313            ret.bottom = ret.top + heightSoFar;
314        } else if (fixed == 2) {
315            ret.left = ret.right - widthSoFar;
316            ret.bottom = ret.top + heightSoFar;
317        } else if (fixed == 4) {
318            ret.left = ret.right - widthSoFar;
319            ret.top = ret.bottom - heightSoFar;
320        } else if (fixed == 6) {
321            ret.right = ret.left + widthSoFar;
322            ret.top = ret.bottom - heightSoFar;
323        }
324        float[] retCorners = CropMath.getCornersFromRect(ret);
325        m0.mapPoints(retCorners);
326        innerRotated = retCorners;
327        // reconstrain to update inner
328        reconstrain();
329    }
330
331    // internal methods
332
333    private boolean isConstrained() {
334        for (int i = 0; i < 8; i += 2) {
335            if (!CropMath.inclusiveContains(outer, innerRotated[i], innerRotated[i + 1]))
336                return false;
337        }
338        return true;
339    }
340
341    private void reconstrain() {
342        // innerRotated has been changed to have incorrect values
343        CropMath.getEdgePoints(outer, innerRotated);
344        Matrix m = getRotMatrix();
345        float[] unrotated = Arrays.copyOf(innerRotated, 8);
346        m.mapPoints(unrotated);
347        inner = CropMath.trapToRect(unrotated);
348    }
349
350    private void rotateInner() {
351        Matrix m = getInverseRotMatrix();
352        m.mapPoints(innerRotated);
353    }
354
355    private Matrix getRotMatrix() {
356        Matrix m = new Matrix();
357        m.setRotate(rot, outer.centerX(), outer.centerY());
358        return m;
359    }
360
361    private Matrix getInverseRotMatrix() {
362        Matrix m = new Matrix();
363        m.setRotate(-rot, outer.centerX(), outer.centerY());
364        return m;
365    }
366}
367