/** * Copyright (C) 2018-present MongoDB, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the Server Side Public License, version 1, * as published by MongoDB, Inc. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Server Side Public License for more details. * * You should have received a copy of the Server Side Public License * along with this program. If not, see * . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the Server Side Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #include #include #include #include "mongo/db/geo/r2_region_coverer.h" #include "mongo/base/init.h" #include "mongo/bson/bsonmisc.h" #include "mongo/db/geo/geometry_container.h" #include "mongo/logv2/log.h" #include "mongo/platform/random.h" #include "mongo/unittest/unittest.h" #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest namespace { using namespace mongo; using mongo::Polygon; // "windows.h" has another Polygon for Windows GDI. std::default_random_engine generator; MONGO_INITIALIZER(R2CellUnion_Test)(InitializerContext* context) { unsigned seed = stdx::chrono::system_clock::now().time_since_epoch().count(); for (size_t i = 2; i < context->args().size(); ++i) { if (context->args()[i - 1] == "--seed") { seed = std::stoul(context->args()[i]); break; } } generator.seed(seed); LOGV2(20640, "R2CellUnion Test - Random Number Generator Seed: {seed}", "seed"_attr = seed); } // Returns an integral number in [lower, upper] template NumType random(NumType lower, NumType upper) { std::uniform_int_distribution distribution(lower, upper); return distribution(generator); } // // GeoHash // TEST(R2RegionCoverer, GeoHashSubdivide) { GeoHash children[4]; // Full plane -> 4 quadrants GeoHash fullPlane; ASSERT_TRUE(fullPlane.subdivide(children)); ASSERT_EQUALS(children[0], GeoHash(0LL, 1u)); // (x, y) : (0, 0) ASSERT_EQUALS(children[1], GeoHash(1LL << 62, 1u)); // (x, y) : (0, 1) ASSERT_EQUALS(children[2], GeoHash(2LL << 62, 1u)); // (x, y) : (1, 0) ASSERT_EQUALS(children[3], GeoHash(3LL << 62, 1u)); // (x, y) : (1, 1) // Small cell: 0...11XX -> 0...11[0-3] const long long cellHash = 3LL << 2; GeoHash cell(cellHash, 31u); ASSERT_TRUE(cell.subdivide(children)); ASSERT_EQUALS(children[0], GeoHash(cellHash, 32u)); // (x, y) : (0, 0) ASSERT_EQUALS(children[1], GeoHash(cellHash + 1, 32u)); // (x, y) : (0, 1) ASSERT_EQUALS(children[2], GeoHash(cellHash + 2, 32u)); // (x, y) : (1, 0) ASSERT_EQUALS(children[3], GeoHash(cellHash + 3, 32u)); // (x, y) : (1, 1) // Smallest cell at finest level cannot subdivide GeoHash leafCell(1LL, 32u); ASSERT_FALSE(leafCell.subdivide(children)); } TEST(R2RegionCoverer, GeoHashUnusedBits) { GeoHash geoHash(5566154225580586776LL, 0u); GeoHash entirePlane; ASSERT_EQUALS(geoHash, entirePlane); } TEST(R2RegionCoverer, GeoHashContains) { GeoHash entirePlane; GeoHash geoHash(5566154225580586776LL, 32u); // An arbitrary random cell // GeoHash contains itself ASSERT_TRUE(entirePlane.contains(entirePlane)); ASSERT_TRUE(geoHash.contains(geoHash)); // Entire plane contains everything ASSERT_TRUE(entirePlane.contains(geoHash)); ASSERT_FALSE(geoHash.contains(entirePlane)); // Positive cases GeoHash parent("0010"); GeoHash child("00100101"); ASSERT_TRUE(parent.contains(parent)); ASSERT_TRUE(parent.contains(child)); ASSERT_TRUE(entirePlane.contains(geoHash)); // Negative cases GeoHash other("01"); ASSERT_FALSE(parent.contains(other)); ASSERT_FALSE(other.contains(parent)); } // // R2RegionCoverer // // Plane boundary, x: [0.0, 100.0], y: [0.0, 100.0] const double MAXBOUND = 100.0; GeoHashConverter::Parameters getConverterParams() { GeoHashConverter::Parameters params; params.bits = 32; params.min = 0.0; params.max = MAXBOUND; const double numBuckets = (1024 * 1024 * 1024 * 4.0); params.scaling = numBuckets / (params.max - params.min); return params; } /** * Test region which mimics the region of a geohash cell. * NOTE: Technically this is not 100% correct, since geohash cells are inclusive on lower and * exclusive on upper edges. For now, this region is just exclusive on all edges. * TODO: Create an explicit HashCell which correctly encapsulates this behavior, push to the * R2Region interface. */ class HashBoxRegion : public R2Region { public: HashBoxRegion(Box box) : _box(box) {} Box getR2Bounds() const { return _box; } bool fastContains(const Box& other) const { return _box.contains(other); } bool fastDisjoint(const Box& other) const { if (!_box.intersects(other)) return true; // Make outer edges exclusive if (_box._max.x == other._min.x || _box._min.x == other._max.x || _box._max.y == other._min.y || _box._min.y == other._max.y) return true; return false; } private: Box _box; }; TEST(R2RegionCoverer, RandomCells) { auto result = GeoHashConverter::createFromParams(getConverterParams()); ASSERT_OK(result.getStatus()); R2RegionCoverer coverer(std::move(result.getValue())); coverer.setMaxCells(1); // Test random cell ids at all levels. for (int i = 0; i < 10000; ++i) { GeoHash id( random(std::numeric_limits::lowest(), std::numeric_limits::max()), random(0U, GeoHash::kMaxBits)); vector covering; Box box = coverer.getHashConverter().unhashToBoxCovering(id); // Since the unhashed box is expanded by the error 8Mu, we need to shrink it. box.fudge(-GeoHashConverter::kMachinePrecision * MAXBOUND * 20); HashBoxRegion region(box); coverer.getCovering(region, &covering); ASSERT_EQUALS(covering.size(), (size_t)1); ASSERT_EQUALS(covering[0], id); } } double randDouble(double lowerBound, double upperBound) { verify(lowerBound <= upperBound); const int NUMBITS = 53; // Random double in [0, 1) long long randLong = random(std::numeric_limits::lowest(), std::numeric_limits::max()); double r = ldexp(static_cast(randLong & ((1ULL << NUMBITS) - 1ULL)), -NUMBITS); return lowerBound + r * (upperBound - lowerBound); } // Check the given region is covered by the covering completely. // cellId is used internally. void checkCellIdCovering(const GeoHashConverter& converter, const R2Region& region, const R2CellUnion& covering, const GeoHash cellId = GeoHash()) { Box cell = converter.unhashToBoxCovering(cellId); // The covering may or may not contain this disjoint cell, we don't care. if (region.fastDisjoint(cell)) return; // If the covering contains this id, that's fine. if (covering.contains(cellId)) return; // The covering doesn't contain this cell, so the region shouldn't contain this cell. if (region.fastContains(cell)) { LOGV2(20641, "covering {covering}", "covering"_attr = covering.toString()); LOGV2(20642, "cellId {cellId}", "cellId"_attr = cellId); } ASSERT_FALSE(region.fastContains(cell)); // The region intersects with this cell. So the covering should intersect with it too. // We need to go deeper until a leaf. When we reach a leaf, it must be caught above // - disjoint with the region, we don't care. // - intersected with the region, contained in the covering. // We can guarantee the disjoint/intersection test is exact, since it's a circle. GeoHash children[4]; ASSERT_TRUE(cellId.subdivide(children)); // Not a leaf for (int i = 0; i < 4; i++) { checkCellIdCovering(converter, region, covering, children[i]); } } void checkCovering(const GeoHashConverter& converter, const R2Region& region, const R2RegionCoverer& coverer, const vector covering) { // Keep track of how many cells have the same coverer.minLevel() ancestor. map minLevelCells; // Check covering's minLevel and maxLevel. for (size_t i = 0; i < covering.size(); ++i) { unsigned int level = covering[i].getBits(); ASSERT_NOT_LESS_THAN(level, coverer.minLevel()); ASSERT_NOT_GREATER_THAN(level, coverer.maxLevel()); minLevelCells[covering[i].parent(coverer.minLevel())] += 1; } if (covering.size() > (unsigned int)coverer.maxCells()) { // If the covering has more than the requested number of cells, then check // that the cell count cannot be reduced by using the parent of some cell. for (map::const_iterator i = minLevelCells.begin(); i != minLevelCells.end(); ++i) { ASSERT_EQUALS(i->second, 1); } } R2CellUnion cellUnion; cellUnion.init(covering); checkCellIdCovering(converter, region, cellUnion); } // Generate a circle within [0, MAXBOUND] GeometryContainer* getRandomCircle(double radius) { ASSERT_LESS_THAN(radius, MAXBOUND / 2); // Format: { $center : [ [-74, 40.74], 10 ] } GeometryContainer* container = new GeometryContainer(); container ->parseFromQuery( BSON("$center" << BSON_ARRAY(BSON_ARRAY(randDouble(radius, MAXBOUND - radius) << randDouble(radius, MAXBOUND - radius)) << radius)) .firstElement()) .transitional_ignore(); return container; } // Test the covering for arbitrary random circle. TEST(R2RegionCoverer, RandomCircles) { auto result = GeoHashConverter::createFromParams(getConverterParams()); ASSERT_OK(result.getStatus()); R2RegionCoverer coverer(std::move(result.getValue())); coverer.setMaxCells(8); for (int i = 0; i < 1000; i++) { // Using R2BoxRegion, the disjoint with circle gives poor results around the corner, // so many small cells are considered as intersected in the priority queue, which is // very slow for larger minLevel (smaller cell). So we limit minLevels in [0, 6]. coverer.setMinLevel(random(0, 6)); coverer.setMaxLevel(coverer.minLevel() + 4); double radius = randDouble(0.0, MAXBOUND / 2); std::unique_ptr geometry(getRandomCircle(radius)); const R2Region& region = geometry->getR2Region(); vector covering; coverer.getCovering(region, &covering); checkCovering(coverer.getHashConverter(), region, coverer, covering); } } // Test the covering for very small circles, since the above test doesn't cover finest cells. TEST(R2RegionCoverer, RandomTinyCircles) { auto result = GeoHashConverter::createFromParams(getConverterParams()); ASSERT_OK(result.getStatus()); R2RegionCoverer coverer(std::move(result.getValue())); coverer.setMaxCells(random(1, 20)); // [1, 20] for (int i = 0; i < 10000; i++) { do { coverer.setMinLevel(random(0U, GeoHash::kMaxBits)); coverer.setMaxLevel(random(0U, GeoHash::kMaxBits)); } while (coverer.minLevel() > coverer.maxLevel()); // 100 * 2 ^ -32 ~= 2.3E-8 (cell edge length) double radius = randDouble(1E-15, ldexp(100.0, -32) * 10); std::unique_ptr geometry(getRandomCircle(radius)); const R2Region& region = geometry->getR2Region(); vector covering; coverer.getCovering(region, &covering); checkCovering(coverer.getHashConverter(), region, coverer, covering); } } // // Shape Intersection // TEST(ShapeIntersection, Lines) { /* * E |D * A___B |C G * F */ Point a(0, 0), b(1, 0), c(2, 0), d(2, 1); Point e(0.5, 1), f(0.5, -0.5), g(3, 0); /* * Basic disjoint * / | * / | */ ASSERT_FALSE(linesIntersect(a, d, c, b)); ASSERT_FALSE(linesIntersect(c, b, a, d)); // commutative /* * Basic disjoint (axis aligned) * | * ___ | */ ASSERT_FALSE(linesIntersect(a, b, c, d)); ASSERT_FALSE(linesIntersect(c, d, a, b)); // commutative /* * Basic intersection * \/ * /\ */ ASSERT_TRUE(linesIntersect(e, c, f, d)); ASSERT_TRUE(linesIntersect(f, d, e, c)); // commutative /* * Basic intersection (axis aligned) * _|_ * | */ ASSERT_TRUE(linesIntersect(a, b, e, f)); ASSERT_TRUE(linesIntersect(f, e, b, a)); // commutative /* * One vertex on the line * \ * ____ \ */ ASSERT_FALSE(linesIntersect(a, b, e, c)); ASSERT_FALSE(linesIntersect(e, c, a, b)); // commutative /* * One vertex on the segment * \ * ___\___ */ ASSERT_TRUE(linesIntersect(a, c, b, e)); ASSERT_TRUE(linesIntersect(e, b, a, c)); // commutative /* * Two segments share one vertex * / * /____ */ ASSERT_TRUE(linesIntersect(a, c, a, e)); ASSERT_TRUE(linesIntersect(a, e, a, c)); // commutative /* * Intersected segments on the same line * A___B===C---G */ ASSERT_TRUE(linesIntersect(a, c, b, g)); ASSERT_TRUE(linesIntersect(b, g, c, a)); // commutative /* * Disjoint segments on the same line * A___B C---G */ ASSERT_FALSE(linesIntersect(a, b, c, g)); ASSERT_FALSE(linesIntersect(c, g, a, b)); // commutative /* * Segments on the same line share one vertex. * /D * /B * F/ */ ASSERT_TRUE(linesIntersect(d, b, b, f)); ASSERT_TRUE(linesIntersect(f, b, d, b)); // commutative // axis aligned ASSERT_TRUE(linesIntersect(a, c, g, c)); ASSERT_TRUE(linesIntersect(c, g, a, c)); // commutative } TEST(ShapeIntersection, Polygons) { // Convex polygon (triangle) /* * Disjoint, bounds disjoint * /| * / | [] * /__| */ vector triangleVetices; triangleVetices.push_back(Point(0, 0)); triangleVetices.push_back(Point(1, 0)); triangleVetices.push_back(Point(1, 4)); Polygon triangle(triangleVetices); Box box; box = Box(1.5, 1.5, 1); ASSERT_FALSE(edgesIntersectsWithBox(triangle.points(), box)); ASSERT_FALSE(polygonIntersectsWithBox(triangle, box)); ASSERT_FALSE(polygonContainsBox(triangle, box)); /* * Disjoint, bounds intersect * [] /| * / | * /__| */ box = Box(-0.5, 3.5, 1); ASSERT_FALSE(edgesIntersectsWithBox(triangle.points(), box)); ASSERT_FALSE(polygonIntersectsWithBox(triangle, box)); ASSERT_FALSE(polygonContainsBox(triangle, box)); /* * Intersect on one polygon vertex * _____ * | | * |_ /|_| * / | * /__| */ box = Box(0, 3, 2); ASSERT_TRUE(edgesIntersectsWithBox(triangle.points(), box)); ASSERT_TRUE(polygonIntersectsWithBox(triangle, box)); ASSERT_FALSE(polygonContainsBox(triangle, box)); /* * Box contains polygon * __________ * | | * | /| | * | / | | * | /__| | * |__________| */ box = Box(-1, -1, 6); ASSERT_FALSE(edgesIntersectsWithBox(triangle.points(), box)); ASSERT_TRUE(polygonIntersectsWithBox(triangle, box)); ASSERT_FALSE(polygonContainsBox(triangle, box)); /* * Polygon contains box * /| * / | * / | * / []| * /____| */ box = Box(0.1, 0.1, 0.2); ASSERT_FALSE(edgesIntersectsWithBox(triangle.points(), box)); ASSERT_TRUE(polygonIntersectsWithBox(triangle, box)); ASSERT_TRUE(polygonContainsBox(triangle, box)); /* * Intersect, but no vertex is contained by the other shape. * ___ /|_ * | / | | * | / | | * |_/___|_| * /____| */ box = Box(0, 1, 2); ASSERT_TRUE(edgesIntersectsWithBox(triangle.points(), box)); ASSERT_TRUE(polygonIntersectsWithBox(triangle, box)); ASSERT_FALSE(polygonContainsBox(triangle, box)); // Concave polygon /* * (0,4) * |\ * | \(1,1) * | `. * |____`. (4,0) * (0,0) */ vector concaveVetices; concaveVetices.push_back(Point(0, 0)); concaveVetices.push_back(Point(4, 0)); concaveVetices.push_back(Point(1, 1)); concaveVetices.push_back(Point(0, 4)); Polygon concave(concaveVetices); /* * Disjoint * |\ * | \ * | `. * |____`. * [] */ box = Box(1, -1, 0.9); ASSERT_FALSE(edgesIntersectsWithBox(concave.points(), box)); ASSERT_FALSE(polygonIntersectsWithBox(concave, box)); ASSERT_FALSE(polygonContainsBox(concave, box)); /* * Disjoint, bounds intersect * |\ * | \[] * | `. * |____`. */ box = Box(1.1, 1.1, 0.2); ASSERT_FALSE(edgesIntersectsWithBox(concave.points(), box)); ASSERT_FALSE(polygonIntersectsWithBox(concave, box)); ASSERT_FALSE(polygonContainsBox(concave, box)); /* * Intersect, one box vertex is contained by the polygon. * |\ * |+\+ (1.5, 1.5) * |+-`. * |____`. */ box = Box(0.5, 0.5, 1); ASSERT_TRUE(edgesIntersectsWithBox(concave.points(), box)); ASSERT_TRUE(polygonIntersectsWithBox(concave, box)); ASSERT_FALSE(polygonContainsBox(concave, box)); /* * Intersect, no vertex is contained by the other shape. * |\ * +| \--+ * || `.| * ||____`. * +-----+ */ box = Box(-0.5, -0.5, 3); ASSERT_TRUE(edgesIntersectsWithBox(concave.points(), box)); ASSERT_TRUE(polygonIntersectsWithBox(concave, box)); ASSERT_FALSE(polygonContainsBox(concave, box)); } TEST(ShapeIntersection, Annulus) { R2Annulus annulus(Point(0.0, 0.0), 1, 5); Box box; // Disjoint, out of outer circle box = Box(4, 4, 1); ASSERT_TRUE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); // Box contains outer circle box = Box(-6, -5.5, 12); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); // Box intersects with the outer circle, but not the inner circle box = Box(3, 3, 4); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); // Box is contained by the annulus box = Box(2, 2, 1); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_TRUE(annulus.fastContains(box)); // Box is contained by the outer circle and intersects with the inner circle box = Box(0.4, 0.5, 3); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); // Box intersects with both outer and inner circle box = Box(-4, -4, 4.5); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); // Box is inside the inner circle box = Box(-0.1, -0.2, 0.5); ASSERT_TRUE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); // Box contains the inner circle, but intersects with the outer circle box = Box(-2, -2, 7); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); // // Annulus contains both inner and outer circles as boundaries. // // Box only touches the outer boundary box = Box(3, 4, 1); // Lower left touches boundary ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); box = Box(-4, -5, 1); // Upper right touches boundary ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); // Box is contained by the annulus touching the outer boundary box = Box(-4, -3, 0.1); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_TRUE(annulus.fastContains(box)); // Box is contained by the annulus touching the inner boundary box = Box(0, 1, 1); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_TRUE(annulus.fastContains(box)); // Box only touches the inner boundary at (-0.6, 0.8) box = Box(-0.6, 0.3, 0.5); ASSERT_FALSE(annulus.fastDisjoint(box)); ASSERT_FALSE(annulus.fastContains(box)); } bool oneIn(unsigned num) { std::uniform_int_distribution distribution(1, num); return distribution(generator) == 1; } void generateRandomCells(GeoHash const& id, bool selected, std::vector* unnormalized, std::vector* normalized) { // This function generates an unnormalized and a normalized GeoHash vector to create // a random R2CellUnion. // If selected is true, the region covered by GeoHash id will be covered by the cells // in unnormalized and normalized. // This is a leaf cell and cannot be subdivided further, so it must be added. if (id.getBits() == 32) { unnormalized->push_back(id); return; } // If the parent cell was not selected, this cell will be selected with a probability // proportional to its level, so smaller cells are more likely to be selected. if (!selected && oneIn(32 - id.getBits())) { normalized->push_back(id); selected = true; } // If this cell is selected, we can either add it or another set of cells that // cover the same region. bool added = false; if (selected && !oneIn(6)) { unnormalized->push_back(id); added = true; } // Add all the children of this cell if it was selected, but not added. // Randomly add other children cells. // Make sure not to include all 4 children if not selected to ensure that the // normalized union is correct. int numChildren = 0; GeoHash children[4]; id.subdivide(children); for (int pos = 0; pos < 4; ++pos) { // If selected, recurse on 4/12 = 1/3 child to add overlapping cells to the // normalized vector. // If not selected, recurse on 4 * 2/7 = 8/7 child. if ((selected ? oneIn(12) : (random(0, 6) < 2)) && numChildren < 3) { generateRandomCells(children[pos], selected, unnormalized, normalized); ++numChildren; } if (selected && !added) { generateRandomCells(children[pos], selected, unnormalized, normalized); } } } TEST(R2CellUnion, Normalize) { int unnormalizedSum = 0, normalizedSum = 0; int kIters = 2000; for (int i = 0; i < kIters; ++i) { std::vector input, expected; generateRandomCells(GeoHash(), false, &input, &expected); unnormalizedSum += input.size(); normalizedSum += expected.size(); // Initialize with unnormalized input R2CellUnion cellUnion; cellUnion.init(input); // Check to make sure the cells in cellUnion equal the expected cells ASSERT_EQUALS(expected.size(), cellUnion.cellIds().size()); for (size_t i = 0; i < expected.size(); ++i) { ASSERT_EQUALS(expected[i], cellUnion.cellIds()[i]); } } LOGV2(20643, "Average Unnormalized Size: {unnormalizedSum_1_0_kIters}", "unnormalizedSum_1_0_kIters"_attr = unnormalizedSum * 1.0 / kIters); LOGV2(20644, "Average Normalized Size: {normalizedSum_1_0_kIters}", "normalizedSum_1_0_kIters"_attr = normalizedSum * 1.0 / kIters); } void testContains(const R2CellUnion& cellUnion, GeoHash id, int num) { // Breadth first check of the child cells to make sure that each one is contained // in cellUnion std::queue ids; ids.push(id); int cellsChecked = 0; while (!ids.empty() && cellsChecked < num) { ++cellsChecked; GeoHash currentId = ids.front(); ids.pop(); ASSERT_TRUE(cellUnion.contains(currentId)); ASSERT_TRUE(cellUnion.intersects(currentId)); if (currentId.getBits() < 32) { GeoHash children[4]; currentId.subdivide(children); for (int i = 0; i < 4; ++i) { ids.push(children[i]); } } } } TEST(R2CellUnion, Contains) { // An R2CellUnion should contain all of its children. std::vector entirePlaneVector; GeoHash entirePlane; entirePlaneVector.push_back(entirePlane); R2CellUnion entirePlaneUnion; entirePlaneUnion.init(entirePlaneVector); ASSERT_TRUE(entirePlaneUnion.contains(entirePlane)); GeoHash childCell1("00"); ASSERT_TRUE(entirePlaneUnion.contains(childCell1)); GeoHash childCell2("01"); ASSERT_TRUE(entirePlaneUnion.contains(childCell2)); GeoHash childCell3("10"); ASSERT_TRUE(entirePlaneUnion.contains(childCell3)); GeoHash childCell4("11"); ASSERT_TRUE(entirePlaneUnion.contains(childCell4)); // An R2CellUnion should contain every cell that is contained by one of its member cells for (int i = 0; i < 2000; ++i) { std::vector unnormalized, normalized; generateRandomCells(GeoHash(), false, &unnormalized, &normalized); R2CellUnion cellUnion; cellUnion.init(normalized); for (auto cellId : normalized) { testContains(cellUnion, cellId, 100); } } } // Naive implementation of intersects to test correctness bool intersects(const R2CellUnion& cellUnion, GeoHash cellId) { for (auto unionCellId : cellUnion.cellIds()) { // Two cells will only intersect if one contains the other if (unionCellId.contains(cellId) || cellId.contains(unionCellId)) { return true; } } return false; } TEST(R2CellUnion, Intersects) { // An R2CellUnion should intersect with every cell it contains. std::vector entirePlaneVector; GeoHash entirePlane; entirePlaneVector.push_back(entirePlane); R2CellUnion entirePlaneUnion; entirePlaneUnion.init(entirePlaneVector); ASSERT_TRUE(entirePlaneUnion.intersects(entirePlane)); GeoHash childCell1("00"); ASSERT_TRUE(entirePlaneUnion.intersects(childCell1)); GeoHash childCell2("01"); ASSERT_TRUE(entirePlaneUnion.intersects(childCell2)); GeoHash childCell3("10"); ASSERT_TRUE(entirePlaneUnion.intersects(childCell3)); GeoHash childCell4("11"); ASSERT_TRUE(entirePlaneUnion.intersects(childCell4)); for (int k = 0; k < 2000; ++k) { R2CellUnion randomUnion; std::vector unnormalized, normalized; generateRandomCells(GeoHash(), false, &unnormalized, &normalized); randomUnion.init(normalized); // An R2CellUnion should intersect with every cell that contains a member of the union. // It should also intersect with cells it contains for (auto cellId : randomUnion.cellIds()) { for (unsigned level = 0; level <= 32; ++level) { ASSERT_TRUE(randomUnion.intersects(GeoHash(cellId.getHash(), level))); } } // Check that the output of intersects matches that of the naive implementation std::vector otherUnnormalized, otherNormalized; generateRandomCells(GeoHash(), false, &otherUnnormalized, &otherNormalized); for (const GeoHash& cellId : otherUnnormalized) { ASSERT_EQUALS(randomUnion.intersects(cellId), intersects(randomUnion, cellId)); } } } void testDifference(std::vector& xCellIds, std::vector& yCellIds) { // Initialize the two cell unions R2CellUnion x, y; x.init(xCellIds); y.init(yCellIds); // Compute the differences x - y and y - x R2CellUnion xMinusY, yMinusX; xMinusY.init(xCellIds); xMinusY.getDifference(y); yMinusX.init(yCellIds); yMinusX.getDifference(x); // Check that x contains x - y and y contains y - x // Check that y doesn't intersect x - y and x doesn't intersect y - x // Check that y - x doesn't intersect with x - y for (size_t i = 0; i < xMinusY.cellIds().size(); ++i) { const GeoHash& cellId = xMinusY.cellIds()[i]; ASSERT_TRUE(x.contains(cellId)); ASSERT_TRUE(x.intersects(cellId)); ASSERT_FALSE(y.contains(cellId)); ASSERT_FALSE(y.intersects(cellId)); ASSERT_FALSE(yMinusX.contains(cellId)); ASSERT_FALSE(yMinusX.intersects(cellId)); } for (size_t i = 0; i < yMinusX.cellIds().size(); ++i) { const GeoHash& cellId = yMinusX.cellIds()[i]; ASSERT_TRUE(y.contains(cellId)); ASSERT_TRUE(y.intersects(cellId)); ASSERT_FALSE(x.contains(cellId)); ASSERT_FALSE(x.intersects(cellId)); ASSERT_FALSE(xMinusY.contains(cellId)); ASSERT_FALSE(xMinusY.intersects(cellId)); } // Check that x - y + y contains x U y and y - x + x contains x U y // Check that x U y contains both x - y + y and y - x + x R2CellUnion xMinusYPlusY, yMinusXPlusX, xUnionY; xMinusYPlusY.init(xMinusY.cellIds()); xMinusYPlusY.add(y.cellIds()); yMinusXPlusX.init(yMinusX.cellIds()); yMinusXPlusX.add(x.cellIds()); xUnionY.init(x.cellIds()); xUnionY.add(y.cellIds()); for (auto cellId : xUnionY.cellIds()) { ASSERT_TRUE(xMinusYPlusY.contains(cellId)); ASSERT_TRUE(yMinusXPlusX.contains(cellId)); } for (auto cellId : xMinusYPlusY.cellIds()) { ASSERT_TRUE(xUnionY.contains(cellId)); } for (auto cellId : yMinusXPlusX.cellIds()) { ASSERT_TRUE(xUnionY.contains(cellId)); } } TEST(R2CellUnion, Difference) { for (int i = 0; i < 2000; ++i) { std::vector xUnnormalized, xNormalized; generateRandomCells(GeoHash(), false, &xUnnormalized, &xNormalized); std::vector yUnnormalized, yNormalized; generateRandomCells(GeoHash(), false, &yUnnormalized, &yNormalized); // Test with two unions that contain each other testDifference(xUnnormalized, xNormalized); // Test with random unions testDifference(xUnnormalized, yUnnormalized); } } TEST(R2CellUnion, Empty) { R2CellUnion emptyUnion; R2CellUnion randomUnion; std::vector unnormalized, normalized; generateRandomCells(GeoHash(), false, &unnormalized, &normalized); randomUnion.init(normalized); // normalize() emptyUnion.init(std::vector()); ASSERT_TRUE(emptyUnion.cellIds().empty()); // contains() and intersects() for (const GeoHash& cellId : unnormalized) { ASSERT_FALSE(emptyUnion.contains(cellId)); ASSERT_FALSE(emptyUnion.intersects(cellId)); } // getDifference() std::vector originalCellIds; std::copy(randomUnion.cellIds().begin(), randomUnion.cellIds().end(), std::back_inserter(originalCellIds)); randomUnion.getDifference(emptyUnion); ASSERT_TRUE(originalCellIds == randomUnion.cellIds()); emptyUnion.getDifference(randomUnion); ASSERT_TRUE(emptyUnion.cellIds().empty()); } TEST(R2CellUnion, Detach) { GeoHash entirePlaneCell; std::vector cellIds; cellIds.push_back(entirePlaneCell); R2CellUnion entirePlaneUnion; entirePlaneUnion.init(cellIds); ASSERT_EQUALS(1UL, entirePlaneUnion.cellIds().size()); ASSERT_EQUALS(entirePlaneCell, entirePlaneUnion.cellIds()[0]); std::vector otherCellIds; otherCellIds.push_back(GeoHash("01")); entirePlaneUnion.detach(&otherCellIds); ASSERT_EQUALS(1UL, otherCellIds.size()); ASSERT_EQUALS(entirePlaneCell, otherCellIds[0]); ASSERT_TRUE(entirePlaneUnion.cellIds().empty()); } } // namespace