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