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/s2closest_point_query.h"
19 
20 #include <memory>
21 #include <vector>
22 
23 #include <gtest/gtest.h>
24 #include "s2/third_party/absl/memory/memory.h"
25 #include "s2/s1angle.h"
26 #include "s2/s2cap.h"
27 #include "s2/s2cell_id.h"
28 #include "s2/s2closest_edge_query_testing.h"
29 #include "s2/s2edge_distances.h"
30 #include "s2/s2loop.h"
31 #include "s2/s2pointutil.h"
32 #include "s2/s2testing.h"
33 
34 using absl::make_unique;
35 using std::pair;
36 using std::unique_ptr;
37 using std::vector;
38 
39 using TestIndex = S2PointIndex<int>;
40 using TestQuery = S2ClosestPointQuery<int>;
41 
TEST(S2ClosestPointQuery,NoPoints)42 TEST(S2ClosestPointQuery, NoPoints) {
43   TestIndex index;
44   TestQuery query(&index);
45   S2ClosestPointQueryPointTarget target(S2Point(1, 0, 0));
46   const auto results = query.FindClosestPoints(&target);
47   EXPECT_EQ(0, results.size());
48 }
49 
TEST(S2ClosestPointQuery,ManyDuplicatePoints)50 TEST(S2ClosestPointQuery, ManyDuplicatePoints) {
51   static const int kNumPoints = 10000;
52   const S2Point kTestPoint(1, 0, 0);
53   TestIndex index;
54   for (int i = 0; i < kNumPoints; ++i) {
55     index.Add(kTestPoint, i);
56   }
57   TestQuery query(&index);
58   S2ClosestPointQueryPointTarget target(kTestPoint);
59   const auto results = query.FindClosestPoints(&target);
60   EXPECT_EQ(kNumPoints, results.size());
61 }
62 
TEST(S2ClosestPointQuery,EmptyTargetOptimized)63 TEST(S2ClosestPointQuery, EmptyTargetOptimized) {
64   // Ensure that the optimized algorithm handles empty targets when a distance
65   // limit is specified.
66   TestIndex index;
67   for (int i = 0; i < 1000; ++i) {
68     index.Add(S2Testing::RandomPoint(), i);
69   }
70   TestQuery query(&index);
71   query.mutable_options()->set_max_distance(S1Angle::Radians(1e-5));
72   MutableS2ShapeIndex target_index;
73   S2ClosestPointQueryShapeIndexTarget target(&target_index);
74   EXPECT_EQ(0, query.FindClosestPoints(&target).size());
75 }
76 
77 // An abstract class that adds points to an S2PointIndex for benchmarking.
78 struct PointIndexFactory {
79  public:
~PointIndexFactoryPointIndexFactory80   virtual ~PointIndexFactory() {}
81 
82   // Requests that approximately "num_points" points located within the given
83   // S2Cap bound should be added to "index".
84   virtual void AddPoints(const S2Cap& index_cap, int num_points,
85                          TestIndex* index) const = 0;
86 };
87 
88 // Generates points that are regularly spaced along a circle.  Points along a
89 // circle are nearly the worst case for distance calculations, since many
90 // points are nearly equidistant from any query point that is not immediately
91 // adjacent to the circle.
92 struct CirclePointIndexFactory : public PointIndexFactory {
AddPointsCirclePointIndexFactory93   void AddPoints(const S2Cap& index_cap, int num_points,
94                  TestIndex* index) const override {
95     vector<S2Point> points = S2Testing::MakeRegularPoints(
96         index_cap.center(), index_cap.GetRadius(), num_points);
97     for (int i = 0; i < points.size(); ++i) {
98       index->Add(points[i], i);
99     }
100   }
101 };
102 
103 // Generates the vertices of a fractal whose convex hull approximately
104 // matches the given cap.
105 struct FractalPointIndexFactory : public PointIndexFactory {
AddPointsFractalPointIndexFactory106   void AddPoints(const S2Cap& index_cap, int num_points,
107                  TestIndex* index) const override {
108     S2Testing::Fractal fractal;
109     fractal.SetLevelForApproxMaxEdges(num_points);
110     fractal.set_fractal_dimension(1.5);
111     unique_ptr<S2Loop> loop(
112         fractal.MakeLoop(S2Testing::GetRandomFrameAt(index_cap.center()),
113                          index_cap.GetRadius()));
114     for (int i = 0; i < loop->num_vertices(); ++i) {
115       index->Add(loop->vertex(i), i);
116     }
117   }
118 };
119 
120 // Generates vertices on a square grid that includes the entire given cap.
121 struct GridPointIndexFactory : public PointIndexFactory {
AddPointsGridPointIndexFactory122   void AddPoints(const S2Cap& index_cap, int num_points,
123                  TestIndex* index) const override {
124     int sqrt_num_points = ceil(sqrt(num_points));
125     Matrix3x3_d frame = S2Testing::GetRandomFrameAt(index_cap.center());
126     double radius = index_cap.GetRadius().radians();
127     double spacing = 2 * radius / sqrt_num_points;
128     for (int i = 0; i < sqrt_num_points; ++i) {
129       for (int j = 0; j < sqrt_num_points; ++j) {
130         S2Point point(tan((i + 0.5) * spacing - radius),
131                       tan((j + 0.5) * spacing - radius), 1.0);
132         index->Add(S2::FromFrame(frame, point.Normalize()),
133                    i * sqrt_num_points + j);
134       }
135     }
136   }
137 };
138 
139 // The approximate radius of S2Cap from which query points are chosen.
140 static const S1Angle kTestCapRadius = S2Testing::KmToAngle(10);
141 
142 // The result format required by CheckDistanceResults() in s2testing.h.
143 using TestingResult = pair<S2MinDistance, int>;
144 
145 // Use "query" to find the closest point(s) to the given target, and extract
146 // the query results into the given vector.  Also verify that the results
147 // satisfy the search criteria.
GetClosestPoints(TestQuery::Target * target,TestQuery * query,vector<TestingResult> * results)148 static void GetClosestPoints(TestQuery::Target* target, TestQuery* query,
149                              vector<TestingResult>* results) {
150   const auto query_results = query->FindClosestPoints(target);
151   EXPECT_LE(query_results.size(), query->options().max_results());
152   const S2Region* region = query->options().region();
153   if (!region && query->options().max_distance() == S1ChordAngle::Infinity()) {
154     // We can predict exactly how many points should be returned.
155     EXPECT_EQ(std::min(query->options().max_results(),
156                        query->index().num_points()),
157               query_results.size());
158   }
159   for (const auto& result : query_results) {
160     // Check that the point satisfies the region() condition.
161     if (region) EXPECT_TRUE(region->Contains(result.point()));
162 
163     // Check that it satisfies the max_distance() condition.
164     EXPECT_LT(result.distance(), query->options().max_distance());
165     results->push_back(TestingResult(result.distance(), result.data()));
166   }
167 }
168 
TestFindClosestPoints(TestQuery::Target * target,TestQuery * query)169 static void TestFindClosestPoints(TestQuery::Target* target, TestQuery *query) {
170   vector<TestingResult> expected, actual;
171   query->mutable_options()->set_use_brute_force(true);
172   GetClosestPoints(target, query, &expected);
173   query->mutable_options()->set_use_brute_force(false);
174   GetClosestPoints(target, query, &actual);
175   EXPECT_TRUE(CheckDistanceResults(expected, actual,
176                                    query->options().max_results(),
177                                    query->options().max_distance(),
178                                    query->options().max_error()))
179       << "max_results=" << query->options().max_results()
180       << ", max_distance=" << query->options().max_distance()
181       << ", max_error=" << query->options().max_error();
182 
183   if (expected.empty()) return;
184 
185   // Note that when options.max_error() > 0, expected[0].distance may not be
186   // the minimum distance.  It is never larger by more than max_error(), but
187   // the actual value also depends on max_results().
188   //
189   // Here we verify that GetDistance() and IsDistanceLess() return results
190   // that are consistent with the max_error() setting.
191   S1ChordAngle max_error = query->options().max_error();
192   S1ChordAngle min_distance = expected[0].first;
193   EXPECT_LE(query->GetDistance(target), min_distance + max_error);
194 
195   // Test IsDistanceLess().
196   EXPECT_FALSE(query->IsDistanceLess(target, min_distance - max_error));
197   EXPECT_TRUE(query->IsConservativeDistanceLessOrEqual(target, min_distance));
198 }
199 
200 // (Note that every query is checked using the brute force algorithm.)
TestWithIndexFactory(const PointIndexFactory & factory,int num_indexes,int num_points,int num_queries)201 static void TestWithIndexFactory(const PointIndexFactory& factory,
202                                  int num_indexes, int num_points,
203                                  int num_queries) {
204   // Build a set of S2PointIndexes containing the desired geometry.
205   vector<S2Cap> index_caps;
206   vector<unique_ptr<TestIndex>> indexes;
207   for (int i = 0; i < num_indexes; ++i) {
208     S2Testing::rnd.Reset(FLAGS_s2_random_seed + i);
209     index_caps.push_back(S2Cap(S2Testing::RandomPoint(), kTestCapRadius));
210     indexes.push_back(make_unique<TestIndex>());
211     factory.AddPoints(index_caps.back(), num_points, indexes.back().get());
212   }
213   for (int i = 0; i < num_queries; ++i) {
214     S2Testing::rnd.Reset(FLAGS_s2_random_seed + i);
215     int i_index = S2Testing::rnd.Uniform(num_indexes);
216     const S2Cap& index_cap = index_caps[i_index];
217 
218     // Choose query points from an area approximately 4x larger than the
219     // geometry being tested.
220     S1Angle query_radius = 2 * index_cap.GetRadius();
221     S2Cap query_cap(index_cap.center(), query_radius);
222     TestQuery query(indexes[i_index].get());
223 
224     // Occasionally we don't set any limit on the number of result points.
225     // (This may return all points if we also don't set a distance limit.)
226     if (!S2Testing::rnd.OneIn(5)) {
227       query.mutable_options()->set_max_results(1 + S2Testing::rnd.Uniform(10));
228     }
229     // We set a distance limit 2/3 of the time.
230     if (!S2Testing::rnd.OneIn(3)) {
231       query.mutable_options()->set_max_distance(
232           S2Testing::rnd.RandDouble() * query_radius);
233     }
234     if (S2Testing::rnd.OneIn(2)) {
235       // Choose a maximum error whose logarithm is uniformly distributed over
236       // a reasonable range, except that it is sometimes zero.
237       query.mutable_options()->set_max_error(S1Angle::Radians(
238           pow(1e-4, S2Testing::rnd.RandDouble()) * query_radius.radians()));
239     }
240     S2LatLngRect filter_rect = S2LatLngRect::FromCenterSize(
241         S2LatLng(S2Testing::SamplePoint(query_cap)),
242         S2LatLng(S2Testing::rnd.RandDouble() * kTestCapRadius,
243                  S2Testing::rnd.RandDouble() * kTestCapRadius));
244     if (S2Testing::rnd.OneIn(5)) {
245       query.mutable_options()->set_region(&filter_rect);
246     }
247     int target_type = S2Testing::rnd.Uniform(4);
248     if (target_type == 0) {
249       // Find the points closest to a given point.
250       S2Point point = S2Testing::SamplePoint(query_cap);
251       TestQuery::PointTarget target(point);
252       TestFindClosestPoints(&target, &query);
253     } else if (target_type == 1) {
254       // Find the points closest to a given edge.
255       S2Point a = S2Testing::SamplePoint(query_cap);
256       S2Point b = S2Testing::SamplePoint(
257           S2Cap(a, pow(1e-4, S2Testing::rnd.RandDouble()) * query_radius));
258       TestQuery::EdgeTarget target(a, b);
259       TestFindClosestPoints(&target, &query);
260     } else if (target_type == 2) {
261       // Find the points closest to a given cell.
262       int min_level = S2::kMaxDiag.GetLevelForMaxValue(query_radius.radians());
263       int level = min_level + S2Testing::rnd.Uniform(
264           S2CellId::kMaxLevel - min_level + 1);
265       S2Point a = S2Testing::SamplePoint(query_cap);
266       S2Cell cell(S2CellId(a).parent(level));
267       TestQuery::CellTarget target(cell);
268       TestFindClosestPoints(&target, &query);
269     } else {
270       S2_DCHECK_EQ(3, target_type);
271       MutableS2ShapeIndex target_index;
272       s2testing::FractalLoopShapeIndexFactory().AddEdges(index_cap, 100,
273                                                          &target_index);
274       TestQuery::ShapeIndexTarget target(&target_index);
275       target.set_include_interiors(S2Testing::rnd.OneIn(2));
276       TestFindClosestPoints(&target, &query);
277     }
278   }
279 }
280 
281 static const int kNumIndexes = 10;
282 static const int kNumPoints = 1000;
283 static const int kNumQueries = 50;
284 
TEST(S2ClosestPointQueryTest,CirclePoints)285 TEST(S2ClosestPointQueryTest, CirclePoints) {
286   TestWithIndexFactory(CirclePointIndexFactory(),
287                        kNumIndexes, kNumPoints, kNumQueries);
288 }
289 
TEST(S2ClosestPointQueryTest,FractalPoints)290 TEST(S2ClosestPointQueryTest, FractalPoints) {
291   TestWithIndexFactory(FractalPointIndexFactory(),
292                        kNumIndexes, kNumPoints, kNumQueries);
293 }
294 
TEST(S2ClosestPointQueryTest,GridPoints)295 TEST(S2ClosestPointQueryTest, GridPoints) {
296   TestWithIndexFactory(GridPointIndexFactory(),
297                        kNumIndexes, kNumPoints, kNumQueries);
298 }
299 
TEST(S2ClosestPointQueryTest,ConservativeCellDistanceIsUsed)300 TEST(S2ClosestPointQueryTest, ConservativeCellDistanceIsUsed) {
301   const int saved_seed = FLAGS_s2_random_seed;
302   // These specific test cases happen to fail if max_error() is not properly
303   // taken into account when measuring distances to S2PointIndex cells.  They
304   // all involve S2ShapeIndexTarget, which takes advantage of max_error() to
305   // optimize its distance calculation.
306   for (int seed : {16, 586, 589, 822, 1959, 2298, 3155, 3490, 3723, 4953}) {
307     FLAGS_s2_random_seed = seed;
308     TestWithIndexFactory(FractalPointIndexFactory(), 5, 100, 10);
309   }
310   FLAGS_s2_random_seed = saved_seed;
311 }
312 
313