001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.math.geometry.shape.util;
031
032import java.util.ArrayList;
033import java.util.Comparator;
034import java.util.List;
035import java.util.Set;
036import java.util.Stack;
037import java.util.TreeSet;
038
039import odk.lang.FastMath;
040
041import org.openimaj.math.geometry.point.Point2d;
042import org.openimaj.math.geometry.shape.Polygon;
043
044/**
045 * Graham Scan convex hull algorithm, based on the implementation by <a
046 * href="https://github.com/bkiers/GrahamScan">Bart Kiers</a>.
047 * 
048 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
049 * @author Bart Kiers
050 */
051public final class GrahamScan {
052        /**
053         * An enum denoting a directional-turn between 3 Point2ds (vectors).
054         */
055        protected static enum Turn {
056                CLOCKWISE, COUNTER_CLOCKWISE, COLLINEAR
057        }
058
059        /**
060         * Returns true iff all Point2ds in <code>Point2ds</code> are collinear.
061         * 
062         * @param Point2ds
063         *            the list of Point2ds.
064         * @return true iff all Point2ds in <code>Point2ds</code> are collinear.
065         */
066        protected static boolean areAllCollinear(List<Point2d> Point2ds) {
067
068                if (Point2ds.size() < 2) {
069                        return true;
070                }
071
072                final Point2d a = Point2ds.get(0);
073                final Point2d b = Point2ds.get(1);
074
075                for (int i = 2; i < Point2ds.size(); i++) {
076
077                        final Point2d c = Point2ds.get(i);
078
079                        if (getTurn(a, b, c) != Turn.COLLINEAR) {
080                                return false;
081                        }
082                }
083
084                return true;
085        }
086
087        /**
088         * Returns the convex hull of the Point2ds created from the list
089         * <code>Point2ds</code>. Note that the first and last Point2d in the
090         * returned <code>List&lt;java.awt.Point2d&gt;</code> are the same Point2d.
091         * 
092         * @param Point2ds
093         *            the list of Point2ds.
094         * @return the convex hull of the Point2ds created from the list
095         *         <code>Point2ds</code>.
096         */
097        public static Polygon getConvexHull(List<Point2d> Point2ds) {
098
099                final List<Point2d> sorted = new ArrayList<Point2d>(getSortedPoint2dSet(Point2ds));
100
101                if (sorted.size() <= 3) {
102                        return new Polygon(Point2ds);
103                }
104
105                if (areAllCollinear(sorted)) {
106                        return new Polygon(Point2ds);
107                }
108
109                final Stack<Point2d> stack = new Stack<Point2d>();
110                stack.push(sorted.get(0));
111                stack.push(sorted.get(1));
112
113                for (int i = 2; i < sorted.size(); i++) {
114
115                        final Point2d head = sorted.get(i);
116                        final Point2d middle = stack.pop();
117                        final Point2d tail = stack.peek();
118
119                        final Turn turn = getTurn(tail, middle, head);
120
121                        switch (turn) {
122                        case COUNTER_CLOCKWISE:
123                                stack.push(middle);
124                                stack.push(head);
125                                break;
126                        case CLOCKWISE:
127                                i--;
128                                break;
129                        case COLLINEAR:
130                                stack.push(head);
131                                break;
132                        }
133                }
134
135                // close the hull
136                stack.push(sorted.get(0));
137
138                return new Polygon(stack);
139        }
140
141        /**
142         * Returns the Point2ds with the lowest y coordinate. In case more than 1
143         * such Point2d exists, the one with the lowest x coordinate is returned.
144         * 
145         * @param Point2ds
146         *            the list of Point2ds to return the lowest Point2d from.
147         * @return the Point2ds with the lowest y coordinate. In case more than 1
148         *         such Point2d exists, the one with the lowest x coordinate is
149         *         returned.
150         */
151        protected static Point2d getLowestPoint2d(List<Point2d> Point2ds) {
152
153                Point2d lowest = Point2ds.get(0);
154
155                for (int i = 1; i < Point2ds.size(); i++) {
156
157                        final Point2d temp = Point2ds.get(i);
158
159                        if (temp.getY() < lowest.getY() || (temp.getY() == lowest.getY() && temp.getX() < lowest.getX())) {
160                                lowest = temp;
161                        }
162                }
163
164                return lowest;
165        }
166
167        /**
168         * Returns a sorted set of Point2ds from the list <code>Point2ds</code>. The
169         * set of Point2ds are sorted in increasing order of the angle they and the
170         * lowest Point2d <tt>P</tt> make with the x-axis. If two (or more) Point2ds
171         * form the same angle towards <tt>P</tt>, the one closest to <tt>P</tt>
172         * comes first.
173         * 
174         * @param Point2ds
175         *            the list of Point2ds to sort.
176         * @return a sorted set of Point2ds from the list <code>Point2ds</code>.
177         * @see GrahamScan#getLowestPoint2d(java.util.List)
178         */
179        protected static Set<Point2d> getSortedPoint2dSet(List<Point2d> Point2ds) {
180
181                final Point2d lowest = getLowestPoint2d(Point2ds);
182
183                final TreeSet<Point2d> set = new TreeSet<Point2d>(new Comparator<Point2d>() {
184                        @Override
185                        public int compare(Point2d a, Point2d b) {
186
187                                if (a == b || a.equals(b)) {
188                                        return 0;
189                                }
190
191                                final double thetaA = FastMath.atan2(a.getY() - lowest.getY(), a.getX() - lowest.getX());
192                                final double thetaB = FastMath.atan2(b.getY() - lowest.getY(), b.getX() - lowest.getX());
193
194                                if (thetaA < thetaB) {
195                                        return -1;
196                                }
197                                else if (thetaA > thetaB) {
198                                        return 1;
199                                }
200                                else {
201                                        // collinear with the 'lowest' Point2d, let the Point2d
202                                        // closest to it come first
203                                        final double distanceA = FastMath.sqrt(((lowest.getX() - a.getX()) * (lowest.getX() - a
204                                                        .getX())) +
205                                                        ((lowest.getY() - a.getY()) * (lowest.getY() - a.getY())));
206                                        final double distanceB = FastMath.sqrt(((lowest.getX() - b.getX()) * (lowest.getX() - b
207                                                        .getX())) +
208                                                        ((lowest.getY() - b.getY()) * (lowest.getY() - b.getY())));
209
210                                        if (distanceA < distanceB) {
211                                                return -1;
212                                        }
213                                        else {
214                                                return 1;
215                                        }
216                                }
217                        }
218                });
219
220                set.addAll(Point2ds);
221
222                return set;
223        }
224
225        /**
226         * Returns the GrahamScan#Turn formed by traversing through the ordered
227         * Point2ds <code>a</code>, <code>b</code> and <code>c</code>. More
228         * specifically, the cross product <tt>C</tt> between the 3 Point2ds
229         * (vectors) is calculated:
230         * 
231         * <tt>(b.getX()-a.getX() * c.getY()-a.getY()) - (b.getY()-a.getY() * c.getX()-a.getX())</tt>
232         * 
233         * and if <tt>C</tt> is less than 0, the turn is CLOCKWISE, if <tt>C</tt> is
234         * more than 0, the turn is COUNTER_CLOCKWISE, else the three Point2ds are
235         * COLLINEAR.
236         * 
237         * @param a
238         *            the starting Point2d.
239         * @param b
240         *            the second Point2d.
241         * @param c
242         *            the end Point2d.
243         * @return the GrahamScan#Turn formed by traversing through the ordered
244         *         Point2ds <code>a</code>, <code>b</code> and <code>c</code>.
245         */
246        protected static Turn getTurn(Point2d a, Point2d b, Point2d c) {
247                final double crossProduct = ((b.getX() - a.getX()) * (c.getY() - a.getY())) -
248                                ((b.getY() - a.getY()) * (c.getX() - a.getX()));
249
250                if (crossProduct > 0) {
251                        return Turn.COUNTER_CLOCKWISE;
252                }
253                else if (crossProduct < 0) {
254                        return Turn.CLOCKWISE;
255                }
256                else {
257                        return Turn.COLLINEAR;
258                }
259        }
260}