1
0
Fork 0
arangodb/tests/Geo/NearUtilsTest.cpp

464 lines
15 KiB
C++

////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2014-2017 ArangoDB GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
///
/// @author Simon Grätzer
////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include <map>
#include "Basics/StringUtils.h"
#include "Geo/GeoJson.h"
#include "Geo/GeoUtils.h"
#include "GeoIndex/Near.h"
#include "Logger/Logger.h"
#include "VocBase/voc-types.h"
#include <s2/s1angle.h>
#include <s2/s2latlng.h>
#include <s2/s2metrics.h>
#include <velocypack/Builder.h>
#include <velocypack/Parser.h>
#include <velocypack/velocypack-aliases.h>
#include <cmath>
#ifdef WIN32
#undef near
#endif
using namespace arangodb;
using namespace arangodb::basics;
// -----------------------------------------------------------------------------
// --SECTION-- private variables
// -----------------------------------------------------------------------------
typedef std::multimap<S2CellId, LocalDocumentId> index_t;
typedef std::map<LocalDocumentId, S2LatLng> coords_t;
// -----------------------------------------------------------------------------
// --SECTION-- private functions
// -----------------------------------------------------------------------------
/// Perform indexx scan
template <typename CMP>
static std::vector<LocalDocumentId> nearSearch(index_t const& index, coords_t const& coords,
geo_index::NearUtils<CMP>& near,
size_t limit) {
std::vector<LocalDocumentId> result;
while (!near.isDone()) {
std::vector<geo::Interval> intervals = near.intervals();
for (geo::Interval const& interval : intervals) {
// seek to first element after or equal interval.min
index_t::const_iterator it = index.lower_bound(interval.range_min);
while (it != index.end() && it->first <= interval.range_max) {
EXPECT_TRUE(it->first >= interval.range_min);
S2Point center = coords.at(it->second).ToPoint();
near.reportFound(it->second, center);
it++;
}
}
near.didScanIntervals(); // calculate new bounds
while (near.hasNearest()) {
geo_index::Document doc = near.nearest();
result.push_back(doc.token);
near.popNearest();
if (result.size() >= limit) {
break;
}
}
if (result.size() >= limit) {
break;
}
}
return result;
}
static std::vector<S2LatLng> convert(coords_t const& coords,
std::vector<LocalDocumentId> const& docs) {
std::vector<S2LatLng> result;
for (LocalDocumentId rid : docs) {
result.push_back(coords.at(rid));
}
return result;
}
// -----------------------------------------------------------------------------
// --SECTION-- test suite
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
/// @brief setup
////////////////////////////////////////////////////////////////////////////////
/* 1-9 some real world distance */
/* 1 is London +51.500000 -0.166666 */
/* 2 is Honolulu +21.306111 -157.859722 */
/* 3 is Auckland -36.916667 +174.783333 */
/* 4 is Jo'burg -26.166667 +28.033333 */
/***********************************/
typedef geo_index::NearUtils<geo_index::DocumentsAscending> AscIterator;
typedef geo_index::NearUtils<geo_index::DocumentsDescending> DescIterator;
class SimpleNearQueriesTest : public ::testing::Test {
protected:
// simulated index
index_t index;
coords_t docs;
size_t counter = 0;
geo::QueryParams params;
SimpleNearQueriesTest() : counter(0) {
// add some entries to it
for (double lat = -90; lat <= 90; ++lat) {
for (double lon = -180; lon <= 180; ++lon) {
// geocol.insert({lat,lon});
S2LatLng cc = S2LatLng::FromDegrees(lat, lon);
S2CellId cell(cc.ToPoint());
EXPECT_TRUE(cell.level() == S2::kMaxCellLevel);
LocalDocumentId rev(counter++);
index.emplace(cell, rev);
docs.emplace(rev, cc);
}
}
EXPECT_TRUE(65341 == counter);
EXPECT_TRUE(docs.size() == counter);
EXPECT_TRUE(index.size() == counter);
params.sorted = true;
params.origin = S2LatLng::FromDegrees(0, 0);
}
};
TEST_F(SimpleNearQueriesTest, query_all_sorted_ascending) {
params.ascending = true;
AscIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, SIZE_MAX);
std::set<LocalDocumentId> unique;
ASSERT_TRUE(result.size() == counter);
double lastRad = 0;
for (LocalDocumentId rev : result) {
// check that we get every document exactly once
ASSERT_TRUE(unique.find(rev) == unique.end());
unique.insert(rev);
// check sort order
S2LatLng const& cords = docs.at(rev);
S2Point pp = cords.ToPoint();
double rad = near.origin().Angle(pp);
bool eq = rad > lastRad || std::fabs(rad - lastRad) <= geo::kRadEps;
ASSERT_TRUE(eq); // rad >= lastRad
lastRad = rad;
}
ASSERT_TRUE(lastRad != 0);
}
TEST_F(SimpleNearQueriesTest, query_all_sorted_ascending_with_limit) {
params.ascending = true;
AscIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, 5);
ASSERT_TRUE(result.size() == 5);
std::vector<S2LatLng> coords = convert(docs, result);
std::sort(coords.begin(), coords.end());
ASSERT_TRUE(coords[0] == S2LatLng::FromDegrees(-1, 0));
ASSERT_TRUE(coords[1] == S2LatLng::FromDegrees(0, -1));
ASSERT_TRUE(coords[2] == S2LatLng::FromDegrees(0, 0));
ASSERT_TRUE(coords[3] == S2LatLng::FromDegrees(0, 1));
ASSERT_TRUE(coords[4] == S2LatLng::FromDegrees(1, 0));
}
TEST_F(SimpleNearQueriesTest, query_sorted_ascending_with_limit_and_max_distance) {
params.ascending = true;
params.maxDistance = 111200.0;
AscIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, 1000);
ASSERT_TRUE(result.size() == 5);
std::vector<S2LatLng> coords = convert(docs, result);
std::sort(coords.begin(), coords.end());
ASSERT_TRUE(coords[0] == S2LatLng::FromDegrees(-1, 0));
ASSERT_TRUE(coords[1] == S2LatLng::FromDegrees(0, -1));
ASSERT_TRUE(coords[2] == S2LatLng::FromDegrees(0, 0));
ASSERT_TRUE(coords[3] == S2LatLng::FromDegrees(0, 1));
ASSERT_TRUE(coords[4] == S2LatLng::FromDegrees(1, 0));
}
TEST_F(SimpleNearQueriesTest, query_sorted_ascending_with_different_initial_delta) {
params.ascending = true;
params.maxDistance = 111200;
AscIterator near(std::move(params));
near.estimateDensity(S2LatLng::FromDegrees(0, 1).ToPoint());
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, 1000);
ASSERT_TRUE(result.size() == 5);
std::vector<S2LatLng> coords = convert(docs, result);
std::sort(coords.begin(), coords.end());
ASSERT_TRUE(coords[0] == S2LatLng::FromDegrees(-1, 0));
ASSERT_TRUE(coords[1] == S2LatLng::FromDegrees(0, -1));
ASSERT_TRUE(coords[2] == S2LatLng::FromDegrees(0, 0));
ASSERT_TRUE(coords[3] == S2LatLng::FromDegrees(0, 1));
ASSERT_TRUE(coords[4] == S2LatLng::FromDegrees(1, 0));
}
TEST_F(SimpleNearQueriesTest, query_all_sorted_descending) {
params.ascending = false;
DescIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, SIZE_MAX);
std::set<LocalDocumentId> unique;
ASSERT_TRUE(result.size() == counter);
double lastRad = geo::kEarthRadiusInMeters;
for (LocalDocumentId rev : result) {
// check that we get every document exactly once
ASSERT_TRUE(unique.find(rev) == unique.end());
unique.insert(rev);
// check sort order
S2LatLng const& coords = docs.at(rev);
double rad = near.origin().Angle(coords.ToPoint());
bool eq = rad < lastRad || std::fabs(rad - lastRad) <= geo::kRadEps;
ASSERT_TRUE(eq); // rad <= lastRad
lastRad = rad;
}
ASSERT_TRUE(lastRad == 0);
}
TEST_F(SimpleNearQueriesTest, query_all_sorted_descending_with_limit) {
params.ascending = false;
DescIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, 5);
std::set<LocalDocumentId> unique;
ASSERT_TRUE(result.size() == 5);
std::vector<S2LatLng> coords = convert(docs, result);
// [0,180], [0,-180] in any order
for (size_t i = 0; i < 2; i++) {
ASSERT_TRUE(coords[i].lat().degrees() == 0.0);
ASSERT_TRUE(std::abs(coords[i].lng().degrees()) == 180.0);
}
}
TEST_F(SimpleNearQueriesTest, query_all_sorted_descending_with_limit_and_max_distance) {
params.ascending = false;
params.maxDistance = 111200;
DescIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, 1000);
std::set<LocalDocumentId> unique;
ASSERT_TRUE(result.size() == 5);
std::vector<S2LatLng> coords = convert(docs, result);
ASSERT_TRUE(coords[4] == S2LatLng::FromDegrees(0, 0));
for (size_t i = 0; i < 4; i++) {
double lat = std::abs(coords[i].lat().degrees()),
lng = std::abs(coords[i].lng().degrees());
ASSERT_TRUE(lat + lng == 1); // lat == 1 => lng == 0, etc
}
}
/* second main batch of tests */
/* insert 10 x 10 array of points near south pole */
/* then do some searches, results checked against */
/* the same run with full table scan */
class QueryPointAroundTest : public ::testing::Test {
protected:
index_t index;
coords_t docs;
size_t counter;
geo::QueryParams params;
QueryPointAroundTest() : counter(0) {
double lat = -89.0;
for (int i = 0; i < 10; i++) {
double lon = 17.0;
for (int j = 0; j < 10; j++) {
// geocol.insert({lat,lon});
S2LatLng cc = S2LatLng::FromDegrees(lat, lon);
S2CellId cell(cc);
EXPECT_TRUE(cell.level() == S2::kMaxCellLevel);
LocalDocumentId rev(counter++);
index.emplace(cell, rev);
docs.emplace(rev, cc);
lon += 1.0;
}
lat += 1.0;
}
params.sorted = true;
params.ascending = true;
}
void checkResult(S2Point const& origin, std::vector<LocalDocumentId> const& result) {
double lastRad = 0;
for (LocalDocumentId const& rev : result) {
// check sort order
S2LatLng const& cords = docs.at(rev);
double rad = origin.Angle(cords.ToPoint());
ASSERT_TRUE(rad >= lastRad);
lastRad = rad;
}
ASSERT_TRUE(lastRad != 0);
};
};
TEST_F(QueryPointAroundTest, southpole_1) {
params.origin = S2LatLng::FromDegrees(-83.2, 19.2);
AscIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, 7);
ASSERT_TRUE(result.size() == 7);
checkResult(near.origin(), result);
}
TEST_F(QueryPointAroundTest, southpole_2) {
params.origin = S2LatLng::FromDegrees(-83.2, 19.2);
AscIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, 110);
ASSERT_TRUE(result.size() == 100);
checkResult(near.origin(), result);
}
TEST_F(QueryPointAroundTest, southpole_3) {
params.origin = S2LatLng::FromDegrees(-89.9, 0);
AscIterator near(std::move(params));
std::vector<LocalDocumentId> result = nearSearch(index, docs, near, 110);
ASSERT_TRUE(result.size() == 100);
checkResult(near.origin(), result);
}
/* third main batch of tests */
/* adding grid of 40x40 point */
/* performing query sorted by result with */
/* */
static std::shared_ptr<VPackBuilder> createBuilder(char const* c) {
VPackOptions options;
options.checkAttributeUniqueness = true;
VPackParser parser(&options);
parser.parse(c);
return parser.steal();
}
class QueryPointsContainedInTest : public ::testing::Test {
protected:
index_t index;
coords_t docs;
size_t counter;
geo::QueryParams params;
QueryPointsContainedInTest() : counter(0) {
// add some entries to it
for (double lat = -40; lat <= 40; ++lat) {
for (double lon = -40; lon <= 40; ++lon) {
// geocol.insert({lat,lon});
S2LatLng cc = S2LatLng::FromDegrees(lat, lon);
S2CellId cell(cc.ToPoint());
EXPECT_TRUE(cell.level() == S2::kMaxCellLevel);
LocalDocumentId rev(counter++);
index.emplace(cell, rev);
docs.emplace(rev, cc);
}
}
EXPECT_TRUE(6561 == counter);
EXPECT_TRUE(docs.size() == counter);
EXPECT_TRUE(index.size() == counter);
params.sorted = true;
params.ascending = true;
params.filterType = geo::FilterType::CONTAINS;
}
void checkResult(std::vector<LocalDocumentId> const& result,
std::vector<std::pair<double, double>> expected) {
ASSERT_TRUE(result.size() == expected.size());
std::vector<std::pair<double, double>> latLngResult;
for (LocalDocumentId const& rev : result) {
// check sort order
S2LatLng const& cords = docs.at(rev);
latLngResult.emplace_back(cords.lat().degrees(), cords.lng().degrees());
}
std::sort(latLngResult.begin(), latLngResult.end());
std::sort(expected.begin(), expected.end());
auto it = latLngResult.begin();
auto it2 = expected.begin();
for (; it != latLngResult.end(); it++) {
double diff = std::fabs(std::max(it->first - it2->first, it->second - it2->second));
ASSERT_TRUE(diff < 0.00001);
it2++;
}
};
};
TEST_F(QueryPointsContainedInTest, polygon) {
auto polygon = createBuilder(R"=({"type": "Polygon", "coordinates":
[[[-11.5, 23.5], [-6, 26], [-10.5, 26.1], [-11.5, 23.5]]]})=");
geo::geojson::parsePolygon(polygon->slice(), params.filterShape);
params.filterShape.updateBounds(params);
AscIterator near(std::move(params));
checkResult(nearSearch(index, docs, near, 10000), {{24.0, -11.0},
{25.0, -10.0},
{25.0, -9.0},
{26.0, -10.0},
{26.0, -9.0},
{26.0, -8.0},
{26.0, -7.0},
{26.0, -6.0}});
}
TEST_F(QueryPointsContainedInTest, rectangle) {
auto rect = createBuilder(R"=({"type": "Polygon", "coordinates":[[[0,0],[1.5,0],[1.5,1.5],[0,1.5],[0,0]]]})=");
geo::geojson::parsePolygon(rect->slice(), params.filterShape);
ASSERT_TRUE(params.filterShape.type() == geo::ShapeContainer::Type::S2_LATLNGRECT);
params.filterShape.updateBounds(params);
AscIterator near(std::move(params));
checkResult(nearSearch(index, docs, near, 10000),
{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}});
}