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 org.openimaj.math.geometry.point.Point2d;
040import org.openimaj.math.geometry.shape.Polygon;
041
042import net.jafama.FastMath;
043
044/**
045 * Graham Scan convex hull algorithm, based on the implementation by
046 * <a 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 returned
090         * <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 such
143         * 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 such
148         *         Point2d exists, the one with the lowest x coordinate is returned.
149         */
150        protected static Point2d getLowestPoint2d(List<Point2d> Point2ds) {
151
152                Point2d lowest = Point2ds.get(0);
153
154                for (int i = 1; i < Point2ds.size(); i++) {
155
156                        final Point2d temp = Point2ds.get(i);
157
158                        if (temp.getY() < lowest.getY() || (temp.getY() == lowest.getY() && temp.getX() < lowest.getX())) {
159                                lowest = temp;
160                        }
161                }
162
163                return lowest;
164        }
165
166        /**
167         * Returns a sorted set of Point2ds from the list <code>Point2ds</code>. The set
168         * of Point2ds are sorted in increasing order of the angle they and the lowest
169         * Point2d <tt>P</tt> make with the x-axis. If two (or more) Point2ds form the
170         * same angle towards <tt>P</tt>, the one closest to <tt>P</tt> comes first.
171         *
172         * @param Point2ds
173         *            the list of Point2ds to sort.
174         * @return a sorted set of Point2ds from the list <code>Point2ds</code>.
175         * @see GrahamScan#getLowestPoint2d(java.util.List)
176         */
177        protected static Set<Point2d> getSortedPoint2dSet(List<Point2d> Point2ds) {
178
179                final Point2d lowest = getLowestPoint2d(Point2ds);
180
181                final TreeSet<Point2d> set = new TreeSet<Point2d>(new Comparator<Point2d>() {
182                        @Override
183                        public int compare(Point2d a, Point2d b) {
184
185                                if (a == b || a.equals(b)) {
186                                        return 0;
187                                }
188
189                                final double thetaA = FastMath.atan2(a.getY() - lowest.getY(), a.getX() - lowest.getX());
190                                final double thetaB = FastMath.atan2(b.getY() - lowest.getY(), b.getX() - lowest.getX());
191
192                                if (thetaA < thetaB) {
193                                        return -1;
194                                } else if (thetaA > thetaB) {
195                                        return 1;
196                                } else {
197                                        // collinear with the 'lowest' Point2d, let the Point2d
198                                        // closest to it come first
199                                        final double distanceA = FastMath.sqrt(((lowest.getX() - a.getX()) * (lowest.getX() - a
200                                                        .getX())) +
201                                                        ((lowest.getY() - a.getY()) * (lowest.getY() - a.getY())));
202                                        final double distanceB = FastMath.sqrt(((lowest.getX() - b.getX()) * (lowest.getX() - b
203                                                        .getX())) +
204                                                        ((lowest.getY() - b.getY()) * (lowest.getY() - b.getY())));
205
206                                        if (distanceA < distanceB) {
207                                                return -1;
208                                        } else {
209                                                return 1;
210                                        }
211                                }
212                        }
213                });
214
215                set.addAll(Point2ds);
216
217                return set;
218        }
219
220        /**
221         * Returns the GrahamScan#Turn formed by traversing through the ordered Point2ds
222         * <code>a</code>, <code>b</code> and <code>c</code>. More specifically, the
223         * cross product <tt>C</tt> between the 3 Point2ds (vectors) is calculated:
224         *
225         * <tt>(b.getX()-a.getX() * c.getY()-a.getY()) - (b.getY()-a.getY() * c.getX()-a.getX())</tt>
226         *
227         * and if <tt>C</tt> is less than 0, the turn is CLOCKWISE, if <tt>C</tt> is
228         * more than 0, the turn is COUNTER_CLOCKWISE, else the three Point2ds are
229         * COLLINEAR.
230         *
231         * @param a
232         *            the starting Point2d.
233         * @param b
234         *            the second Point2d.
235         * @param c
236         *            the end Point2d.
237         * @return the GrahamScan#Turn formed by traversing through the ordered Point2ds
238         *         <code>a</code>, <code>b</code> and <code>c</code>.
239         */
240        protected static Turn getTurn(Point2d a, Point2d b, Point2d c) {
241                final double crossProduct = ((b.getX() - a.getX()) * (c.getY() - a.getY())) -
242                                ((b.getY() - a.getY()) * (c.getX() - a.getX()));
243
244                if (crossProduct > 0) {
245                        return Turn.COUNTER_CLOCKWISE;
246                } else if (crossProduct < 0) {
247                        return Turn.CLOCKWISE;
248                } else {
249                        return Turn.COLLINEAR;
250                }
251        }
252}