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  */
16 package com.android.gallery3d.filtershow.crop;
17 
18 import android.graphics.Matrix;
19 import android.graphics.Rect;
20 import android.graphics.RectF;
21 
22 import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
23 
24 import java.util.Arrays;
25 
26 /**
27  * Maintains invariant that inner rectangle is constrained to be within the
28  * outer, rotated rectangle.
29  */
30 public class BoundedRect {
31     private float rot;
32     private RectF outer;
33     private RectF inner;
34     private float[] innerRotated;
35 
BoundedRect(float rotation, Rect outerRect, Rect innerRect)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 
BoundedRect(float rotation, RectF outerRect, RectF innerRect)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 
resetTo(float rotation, RectF outerRect, RectF innerRect)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      */
setInner(RectF newInner)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      */
setRotation(float rotation)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 
setToInner(RectF r)92     public void setToInner(RectF r) {
93         r.set(inner);
94     }
95 
setToOuter(RectF r)96     public void setToOuter(RectF r) {
97         r.set(outer);
98     }
99 
getInner()100     public RectF getInner() {
101         return new RectF(inner);
102     }
103 
getOuter()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      */
moveInner(float dx, float dy)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      */
resizeInner(RectF newInner)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      */
fixedAspectResizeInner(RectF newInner)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 
isConstrained()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 
reconstrain()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 
rotateInner()352     private void rotateInner() {
353         Matrix m = getInverseRotMatrix();
354         m.mapPoints(innerRotated);
355     }
356 
getRotMatrix()357     private Matrix getRotMatrix() {
358         Matrix m = new Matrix();
359         m.setRotate(rot, outer.centerX(), outer.centerY());
360         return m;
361     }
362 
getInverseRotMatrix()363     private Matrix getInverseRotMatrix() {
364         Matrix m = new Matrix();
365         m.setRotate(-rot, outer.centerX(), outer.centerY());
366         return m;
367     }
368 }
369