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