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