/* * Convex hull algorithm - Test suite (C++) * * Copyright (c) 2022 Project Nayuki * https://www.nayuki.io/page/convex-hull-algorithm * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program (see COPYING.txt and COPYING.LESSER.txt). * If not, see . */ #include #include #include #include #include #include #include #include "ConvexHull.hpp" using std::size_t; using std::vector; // Forward declarations static void testEmpty(); static void testOne(); static void testTwoDuplicate(); static void testTwoHorizontal0(); static void testTwoHorizontal1(); static void testTwoVertical0(); static void testTwoVertical1(); static void testTwoDiagonal0(); static void testTwoDiagonal1(); static void testRectangle(); static void testHorizontalRandomly(); static void testVerticalRandomly(); static void testVsNaiveRandomly(); static void testHullPropertiesRandomly(); static vector makeHullNaive(const vector &points); static bool isPolygonConvex(const vector &points); static bool isPointInConvexPolygon(const vector &polygon, const Point &point); static int signum(double x); // Random number generation global variables static std::default_random_engine randGen((std::random_device())()); static std::bernoulli_distribution boolDist; static std::normal_distribution normalDist; int main() { try { testEmpty(); testOne(); testTwoDuplicate(); testTwoHorizontal0(); testTwoHorizontal1(); testTwoVertical0(); testTwoVertical1(); testTwoDiagonal0(); testTwoDiagonal1(); testRectangle(); testHorizontalRandomly(); testVerticalRandomly(); testVsNaiveRandomly(); testHullPropertiesRandomly(); std::cerr << "Test passed" << std::endl; return EXIT_SUCCESS; } catch (std::exception &e) { std::cerr << e.what() << std::endl; return EXIT_FAILURE; } } /*---- Fixed test vectors ----*/ static void testEmpty() { const vector points{}; const vector actual = makeConvexHull(points); const vector expect = points; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testOne() { const vector points{Point{3, 1}}; const vector actual = makeConvexHull(points); const vector expect = points; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testTwoDuplicate() { const vector points{Point{0, 0}, Point{0, 0}}; const vector actual = makeConvexHull(points); const vector expect{Point{0, 0}}; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testTwoHorizontal0() { const vector points{Point{2, 0}, Point{5, 0}}; const vector actual = makeConvexHull(points); const vector expect = points; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testTwoHorizontal1() { const vector points{Point{-6, -3}, Point{-8, -3}}; const vector actual = makeConvexHull(points); const vector expect{Point{-8, -3}, Point{-6, -3}}; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testTwoVertical0() { const vector points{Point{1, -4}, Point{1, 4}}; const vector actual = makeConvexHull(points); const vector expect = points; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testTwoVertical1() { const vector points{Point{-1, 2}, Point{-1, -3}}; const vector actual = makeConvexHull(points); const vector expect{Point{-1, -3}, Point{-1, 2}}; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testTwoDiagonal0() { const vector points{Point{-2, -3}, Point{2, 0}}; const vector actual = makeConvexHull(points); const vector expect = points; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testTwoDiagonal1() { const vector points{Point{-2, 3}, Point{2, 0}}; const vector actual = makeConvexHull(points); const vector expect = points; if (actual != expect) throw std::runtime_error("Value mismatch"); } static void testRectangle() { const vector points{Point{-3, 2}, Point{1, 2}, Point{1, -4}, Point{-3, -4}}; const vector actual = makeConvexHull(points); const vector expect{Point{-3, -4}, Point{-3, 2}, Point{1, 2}, Point{1, -4}}; if (actual != expect) throw std::runtime_error("Value mismatch"); } /*---- Randomized testing ----*/ static void testHorizontalRandomly() { const long TRIALS = 100000; std::uniform_int_distribution numPointsDist(1, 30); for (long i = 0; i < TRIALS; i++) { size_t len = numPointsDist(randGen); vector points; if (boolDist(randGen)) { double y = normalDist(randGen); for (size_t j = 0; j < len; j++) points.push_back(Point{normalDist(randGen), y}); } else { std::uniform_int_distribution valDist(-10, 9); double y = valDist(randGen); for (size_t j = 0; j < len; j++) points.push_back(Point{static_cast(valDist(randGen)), y}); } const vector actual = makeConvexHull(points); vector expected{*std::min_element(points.cbegin(), points.cend())}; const Point &temp = *std::max_element(points.cbegin(), points.cend()); if (temp != expected.front()) expected.push_back(temp); if (actual != expected) throw std::runtime_error("Value mismatch"); } } static void testVerticalRandomly() { const long TRIALS = 100000; std::uniform_int_distribution numPointsDist(1, 30); for (long i = 0; i < TRIALS; i++) { size_t len = numPointsDist(randGen); vector points; if (boolDist(randGen)) { double x = normalDist(randGen); for (size_t j = 0; j < len; j++) points.push_back(Point{x, normalDist(randGen)}); } else { std::uniform_int_distribution valDist(-10, 9); double x = valDist(randGen); for (size_t j = 0; j < len; j++) points.push_back(Point{x, static_cast(valDist(randGen))}); } const vector actual = makeConvexHull(points); vector expected{*std::min_element(points.cbegin(), points.cend())}; const Point &temp = *std::max_element(points.cbegin(), points.cend()); if (temp != expected.front()) expected.push_back(temp); if (actual != expected) throw std::runtime_error("Value mismatch"); } } static void testVsNaiveRandomly() { const long TRIALS = 100000; std::uniform_int_distribution numPointsDist(0, 99); for (long i = 0; i < TRIALS; i++) { size_t len = numPointsDist(randGen); vector points; if (boolDist(randGen)) { for (size_t j = 0; j < len; j++) points.push_back(Point{normalDist(randGen), normalDist(randGen)}); } else { std::uniform_int_distribution valDist(0, 9); for (size_t j = 0; j < len; j++) points.push_back(Point{static_cast(valDist(randGen)), static_cast(valDist(randGen))}); } const vector expected = makeHullNaive(points); const vector actual = makeConvexHull(std::move(points)); if (actual != expected) throw std::runtime_error("Value mismatch"); } } static void testHullPropertiesRandomly() { const long TRIALS = 100000; std::uniform_int_distribution numPointsDist(0, 99); std::uniform_int_distribution dupePointsDist(1, 10); for (long i = 0; i < TRIALS; i++) { // Generate random points size_t len = numPointsDist(randGen); vector points; if (boolDist(randGen)) { for (size_t j = 0; j < len; j++) points.push_back(Point{normalDist(randGen), normalDist(randGen)}); } else { std::uniform_int_distribution valDist(0, 9); for (size_t j = 0; j < len; j++) points.push_back(Point{static_cast(valDist(randGen)), static_cast(valDist(randGen))}); } // Compute hull and check properties const vector hull = makeConvexHull(points); if (!isPolygonConvex(hull)) throw std::runtime_error("Polygon not convex"); for (const Point &p : points) { if (!isPointInConvexPolygon(hull, p)) throw std::runtime_error("Point not in polygon"); } // Add duplicate points and check new hull if (!points.empty()) { size_t dupe = dupePointsDist(randGen); for (size_t j = 0; j < dupe; j++) { std::uniform_int_distribution sizeDist(0, points.size() - 1); points.push_back(points.at(sizeDist(randGen))); } const vector nextHull = makeConvexHull(std::move(points)); if (nextHull != hull) throw std::runtime_error("Value mismatch"); } } } static vector makeHullNaive(const vector &points) { if (points.size() <= 1) return vector(points); // Jarvis march / gift wrapping algorithm vector result; const Point *point = &*std::min_element(points.cbegin(), points.cend()); do { result.push_back(*point); const Point *next = &points.front(); for (const Point &p : points) { double ax = next->x - point->x; double ay = next->y - point->y; double bx = p.x - point->x; double by = p.y - point->y; double cross = ax * by - ay * bx; if (cross > 0 || (cross == 0 && bx * bx + by * by > ax * ax + ay * ay)) next = &p; } point = next; } while (*point != result.front()); return result; } static bool isPolygonConvex(const vector &points) { int state = 0; for (size_t i = 0; i + 2 < points.size(); i++) { const Point &p = points.at(i + 0); const Point &q = points.at(i + 1); const Point &r = points.at(i + 2); int sign = signum((q.x - p.x) * (r.y - q.y) - (q.y - p.y) * (r.x - q.x)); if (sign == 0) continue; else if (state == 0) state = sign; else if (sign != state) return false; } return true; } static bool isPointInConvexPolygon(const vector &polygon, const Point &point) { int state = 0; for (size_t i = 0; i < polygon.size(); i++) { const Point &p = polygon.at(i); const Point &q = polygon.at((i + 1) % polygon.size()); int sign = signum((q.x - p.x) * (point.y - q.y) - (q.y - p.y) * (point.x - q.x)); if (sign == 0) continue; else if (state == 0) state = sign; else if (sign != state) return false; } return true; } static int signum(double x) { if (x > 0) return +1; else if (x < 0) return -1; else return 0; }