1 // Copyright 2015 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS-IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 //
15
16 // Author: ericv@google.com (Eric Veach)
17
18 #include "s2/s2convex_hull_query.h"
19
20 #include <cmath>
21 #include <memory>
22
23 #include <gtest/gtest.h>
24 #include "s2/s2testing.h"
25 #include "s2/s2text_format.h"
26
27 using std::fabs;
28 using std::unique_ptr;
29 using std::vector;
30
31 namespace {
32
TEST(S2ConvexHullQueryTest,NoPoints)33 TEST(S2ConvexHullQueryTest, NoPoints) {
34 S2ConvexHullQuery query;
35 unique_ptr<S2Loop> result(query.GetConvexHull());
36 EXPECT_TRUE(result->is_empty());
37 }
38
LoopHasVertex(const S2Loop & loop,const S2Point & p)39 static bool LoopHasVertex(const S2Loop& loop, const S2Point& p) {
40 for (int i = 0; i < loop.num_vertices(); ++i) {
41 if (loop.vertex(i) == p) return true;
42 }
43 return false;
44 }
45
TEST(S2ConvexHullQueryTest,OnePoint)46 TEST(S2ConvexHullQueryTest, OnePoint) {
47 S2ConvexHullQuery query;
48 S2Point p(0, 0, 1);
49 query.AddPoint(p);
50 unique_ptr<S2Loop> result(query.GetConvexHull());
51 EXPECT_EQ(3, result->num_vertices());
52 EXPECT_TRUE(result->IsNormalized());
53 EXPECT_TRUE(LoopHasVertex(*result, p));
54 // Add some duplicate points and check that the result is the same.
55 query.AddPoint(p);
56 query.AddPoint(p);
57 unique_ptr<S2Loop> result2(query.GetConvexHull());
58 EXPECT_TRUE(result2->Equals(result.get()));
59 }
60
TEST(S2ConvexHullQueryTest,TwoPoints)61 TEST(S2ConvexHullQueryTest, TwoPoints) {
62 S2ConvexHullQuery query;
63 S2Point p(0, 0, 1);
64 S2Point q(0, 1, 0);
65 query.AddPoint(p);
66 query.AddPoint(q);
67 unique_ptr<S2Loop> result(query.GetConvexHull());
68 EXPECT_EQ(3, result->num_vertices());
69 EXPECT_TRUE(result->IsNormalized());
70 EXPECT_TRUE(LoopHasVertex(*result, p));
71 EXPECT_TRUE(LoopHasVertex(*result, q));
72 // Add some duplicate points and check that the result is the same.
73 query.AddPoint(q);
74 query.AddPoint(p);
75 query.AddPoint(p);
76 unique_ptr<S2Loop> result2(query.GetConvexHull());
77 EXPECT_TRUE(result2->Equals(result.get()));
78 }
79
TEST(S2ConvexHullQueryTest,EmptyLoop)80 TEST(S2ConvexHullQueryTest, EmptyLoop) {
81 S2ConvexHullQuery query;
82 S2Loop empty(S2Loop::kEmpty());
83 query.AddLoop(empty);
84 unique_ptr<S2Loop> result(query.GetConvexHull());
85 EXPECT_TRUE(result->is_empty());
86 }
87
TEST(S2ConvexHullQueryTest,FullLoop)88 TEST(S2ConvexHullQueryTest, FullLoop) {
89 S2ConvexHullQuery query;
90 S2Loop full(S2Loop::kFull());
91 query.AddLoop(full);
92 unique_ptr<S2Loop> result(query.GetConvexHull());
93 EXPECT_TRUE(result->is_full());
94 }
95
TEST(S2ConvexHullQueryTest,EmptyPolygon)96 TEST(S2ConvexHullQueryTest, EmptyPolygon) {
97 S2ConvexHullQuery query;
98 vector<unique_ptr<S2Loop>> loops;
99 S2Polygon empty(std::move(loops));
100 query.AddPolygon(empty);
101 unique_ptr<S2Loop> result(query.GetConvexHull());
102 EXPECT_TRUE(result->is_empty());
103 }
104
TEST(S2ConvexHullQueryTest,NonConvexPoints)105 TEST(S2ConvexHullQueryTest, NonConvexPoints) {
106 // Generate a point set such that the only convex region containing them is
107 // the entire sphere. In other words, you can generate any point on the
108 // sphere by repeatedly linearly interpolating between the points. (The
109 // four points of a tetrahedron would also work, but this is easier.)
110 S2ConvexHullQuery query;
111 for (int face = 0; face < 6; ++face) {
112 query.AddPoint(S2CellId::FromFace(face).ToPoint());
113 }
114 unique_ptr<S2Loop> result(query.GetConvexHull());
115 EXPECT_TRUE(result->is_full());
116 }
117
TEST(S2ConvexHullQueryTest,SimplePolyline)118 TEST(S2ConvexHullQueryTest, SimplePolyline) {
119 // A polyline is handling identically to a point set, so there is no need
120 // for special testing other than code coverage.
121 unique_ptr<S2Polyline> polyline(s2textformat::MakePolyline(
122 "0:1, 0:9, 1:6, 2:6, 3:10, 4:10, 5:5, 4:0, 3:0, 2:5, 1:5"));
123 S2ConvexHullQuery query;
124 query.AddPolyline(*polyline);
125 unique_ptr<S2Loop> result(query.GetConvexHull());
126 unique_ptr<S2Loop> expected_result(
127 s2textformat::MakeLoop("0:1, 0:9, 3:10, 4:10, 5:5, 4:0, 3:0"));
128 EXPECT_TRUE(result->BoundaryEquals(expected_result.get()));
129 }
130
TestNorthPoleLoop(S1Angle radius,int num_vertices)131 void TestNorthPoleLoop(S1Angle radius, int num_vertices) {
132 // If the radius is very close to 90, then it's hard to predict whether the
133 // result will be the full loop or not.
134 S2_DCHECK_GE(fabs(radius.radians() - M_PI_2), 1e-15);
135
136 S2ConvexHullQuery query;
137 unique_ptr<S2Loop> loop(
138 S2Loop::MakeRegularLoop(S2Point(0, 0, 1), radius, num_vertices));
139 query.AddLoop(*loop);
140 unique_ptr<S2Loop> result(query.GetConvexHull());
141 if (radius > S1Angle::Radians(M_PI_2)) {
142 EXPECT_TRUE(result->is_full());
143 } else {
144 EXPECT_TRUE(result->BoundaryEquals(loop.get()));
145 }
146 }
147
148
TEST(S2ConvexHullQueryTest,LoopsAroundNorthPole)149 TEST(S2ConvexHullQueryTest, LoopsAroundNorthPole) {
150 // Test loops of various sizes around the north pole.
151 TestNorthPoleLoop(S1Angle::Degrees(1), 3);
152 TestNorthPoleLoop(S1Angle::Degrees(89), 3);
153
154 // The following two loops should yield the full loop.
155 TestNorthPoleLoop(S1Angle::Degrees(91), 3);
156 TestNorthPoleLoop(S1Angle::Degrees(179), 3);
157
158 TestNorthPoleLoop(S1Angle::Degrees(10), 100);
159 TestNorthPoleLoop(S1Angle::Degrees(89), 1000);
160 }
161
TEST(S2ConvexHullQueryTest,PointsInsideHull)162 TEST(S2ConvexHullQueryTest, PointsInsideHull) {
163 // Repeatedly build the convex hull of a set of points, then add more points
164 // inside that loop and build the convex hull again. The result should
165 // always be the same.
166 const int kIters = 1000;
167 for (int iter = 0; iter < kIters; ++iter) {
168 S2Testing::rnd.Reset(iter + 1); // Easier to reproduce a specific case.
169
170 // Choose points from within a cap of random size, up to but not including
171 // an entire hemisphere.
172 S2Cap cap = S2Testing::GetRandomCap(1e-15, 1.999 * M_PI);
173 S2ConvexHullQuery query;
174 int num_points1 = S2Testing::rnd.Uniform(100) + 3;
175 for (int i = 0; i < num_points1; ++i) {
176 query.AddPoint(S2Testing::SamplePoint(cap));
177 }
178 unique_ptr<S2Loop> hull(query.GetConvexHull());
179
180 // When the convex hull is nearly a hemisphere, the algorithm sometimes
181 // returns a full cap instead. This is because it first computes a
182 // bounding rectangle for all the input points/edges and then converts it
183 // to a bounding cap, which sometimes yields a non-convex cap (radius
184 // larger than 90 degrees). This should not be a problem in practice
185 // (since most convex hulls are not hemispheres), but in order make this
186 // test pass reliably it means that we need to reject convex hulls whose
187 // bounding cap (when computed from a bounding rectangle) is not convex.
188 //
189 // TODO(ericv): This test can still fail (about 1 iteration in 500,000)
190 // because the S2LatLngRect::GetCapBound implementation does not guarantee
191 // that A.Contains(B) implies A.GetCapBound().Contains(B.GetCapBound()).
192 if (hull->GetCapBound().height() >= 1) continue;
193
194 // Otherwise, add more points inside the convex hull.
195 const int num_points2 = 1000;
196 for (int i = 0; i < num_points2; ++i) {
197 S2Point p = S2Testing::SamplePoint(cap);
198 if (hull->Contains(p)) {
199 query.AddPoint(p);
200 }
201 }
202 // Finally, build a new convex hull and check that it hasn't changed.
203 unique_ptr<S2Loop> hull2(query.GetConvexHull());
204 EXPECT_TRUE(hull2->BoundaryEquals(hull.get())) << "Iteration: " << iter;
205 }
206 }
207
208 } // namespace
209