1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 /*
19 * This is not the original file distributed by the Apache Software Foundation
20 * It has been modified by the Hipparchus project
21 */
22 package org.hipparchus.geometry.euclidean.twod.hull;
23
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Comparator;
27 import java.util.List;
28
29 import org.hipparchus.geometry.euclidean.twod.Line;
30 import org.hipparchus.geometry.euclidean.twod.Vector2D;
31 import org.hipparchus.util.FastMath;
32 import org.hipparchus.util.Precision;
33
34 /**
35 * Implements Andrew's monotone chain method to generate the convex hull of a finite set of
36 * points in the two-dimensional euclidean space.
37 * <p>
38 * The runtime complexity is O(n log n), with n being the number of input points. If the
39 * point set is already sorted (by x-coordinate), the runtime complexity is O(n).
40 * <p>
41 * The implementation is not sensitive to collinear points on the hull. The parameter
42 * {@code includeCollinearPoints} allows to control the behavior with regard to collinear points.
43 * If {@code true}, all points on the boundary of the hull will be added to the hull vertices,
44 * otherwise only the extreme points will be present. By default, collinear points are not added
45 * as hull vertices.
46 * <p>
47 * The {@code tolerance} parameter (default: 1e-10) is used as epsilon criteria to determine
48 * identical and collinear points.
49 *
50 * @see <a href="http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain">
51 * Andrew's monotone chain algorithm (Wikibooks)</a>
52 */
53 public class MonotoneChain extends AbstractConvexHullGenerator2D {
54
55 /**
56 * Create a new MonotoneChain instance.
57 */
58 public MonotoneChain() {
59 this(false);
60 }
61
62 /**
63 * Create a new MonotoneChain instance.
64 * @param includeCollinearPoints whether collinear points shall be added as hull vertices
65 */
66 public MonotoneChain(final boolean includeCollinearPoints) {
67 super(includeCollinearPoints);
68 }
69
70 /**
71 * Create a new MonotoneChain instance.
72 * @param includeCollinearPoints whether collinear points shall be added as hull vertices
73 * @param tolerance tolerance below which points are considered identical
74 */
75 public MonotoneChain(final boolean includeCollinearPoints, final double tolerance) {
76 super(includeCollinearPoints, tolerance);
77 }
78
79 /** {@inheritDoc} */
80 @Override
81 public Collection<Vector2D> findHullVertices(final Collection<Vector2D> points) {
82
83 final List<Vector2D> pointsSortedByXAxis = new ArrayList<>(points);
84
85 // sort the points in increasing order on the x-axis
86 pointsSortedByXAxis.sort(new Comparator<Vector2D>() {
87 /** {@inheritDoc} */
88 @Override
89 public int compare(final Vector2D o1, final Vector2D o2) {
90 final double tolerance = getTolerance();
91 // need to take the tolerance value into account, otherwise collinear points
92 // will not be handled correctly when building the upper/lower hull
93 final int diff = Precision.compareTo(o1.getX(), o2.getX(), tolerance);
94 if (diff == 0) {
95 return Precision.compareTo(o1.getY(), o2.getY(), tolerance);
96 } else {
97 return diff;
98 }
99 }
100 });
101
102 // build lower hull
103 final List<Vector2D> lowerHull = new ArrayList<>();
104 for (Vector2D p : pointsSortedByXAxis) {
105 updateHull(p, lowerHull);
106 }
107
108 // build upper hull
109 final List<Vector2D> upperHull = new ArrayList<>();
110 for (int idx = pointsSortedByXAxis.size() - 1; idx >= 0; idx--) {
111 final Vector2D p = pointsSortedByXAxis.get(idx);
112 updateHull(p, upperHull);
113 }
114
115 // concatenate the lower and upper hulls
116 // the last point of each list is omitted as it is repeated at the beginning of the other list
117 final List<Vector2D> hullVertices = new ArrayList<>(lowerHull.size() + upperHull.size() - 2);
118 for (int idx = 0; idx < lowerHull.size() - 1; idx++) {
119 hullVertices.add(lowerHull.get(idx));
120 }
121 for (int idx = 0; idx < upperHull.size() - 1; idx++) {
122 hullVertices.add(upperHull.get(idx));
123 }
124
125 // special case: if the lower and upper hull may contain only 1 point if all are identical
126 if (hullVertices.isEmpty() && ! lowerHull.isEmpty()) {
127 hullVertices.add(lowerHull.get(0));
128 }
129
130 return hullVertices;
131 }
132
133 /**
134 * Update the partial hull with the current point.
135 *
136 * @param point the current point
137 * @param hull the partial hull
138 */
139 private void updateHull(final Vector2D point, final List<Vector2D> hull) {
140 final double tolerance = getTolerance();
141
142 if (hull.size() == 1) {
143 // ensure that we do not add an identical point
144 final Vector2D p1 = hull.get(0);
145 if (p1.distance(point) < tolerance) {
146 return;
147 }
148 }
149
150 while (hull.size() >= 2) {
151 final int size = hull.size();
152 final Vector2D p1 = hull.get(size - 2);
153 final Vector2D p2 = hull.get(size - 1);
154
155 final double offset = new Line(p1, p2, tolerance).getOffset(point);
156 if (FastMath.abs(offset) < tolerance) {
157 // the point is collinear to the line (p1, p2)
158
159 final double distanceToCurrent = p1.distance(point);
160 if (distanceToCurrent < tolerance || p2.distance(point) < tolerance) {
161 // the point is assumed to be identical to either p1 or p2
162 return;
163 }
164
165 final double distanceToLast = p1.distance(p2);
166 if (isIncludeCollinearPoints()) {
167 final int index = distanceToCurrent < distanceToLast ? size - 1 : size;
168 hull.add(index, point);
169 } else {
170 if (distanceToCurrent > distanceToLast) {
171 hull.remove(size - 1);
172 hull.add(point);
173 }
174 }
175 return;
176 } else if (offset > 0) {
177 hull.remove(size - 1);
178 } else {
179 break;
180 }
181 }
182 hull.add(point);
183 }
184
185 }