/* * Convex hull algorithm - Test suite (C#) * * Copyright (c) 2017 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 . */ using System; using System.Collections.Generic; using System.Linq; public sealed class ConvexHullTest { public static void Main(string[] args) { TestEmpty(); TestOne(); TestTwoDuplicate(); TestTwoHorizontal0(); TestTwoHorizontal1(); TestTwoVertical0(); TestTwoVertical1(); TestTwoDiagonal0(); TestTwoDiagonal1(); TestRectangle(); TestHorizontalRandomly(); TestVerticalRandomly(); TestVsNaiveRandomly(); TestHullPropertiesRandomly(); } /*---- Fixed test vectors ----*/ private static void TestEmpty() { IList points = new List(); IList actual = ConvexHull.MakeHull(points); IList expect = new List(); AssertEquals(expect, actual); } private static void TestOne() { IList points = new List{new Point(3, 1)}; IList actual = ConvexHull.MakeHull(points); IList expect = points; AssertEquals(expect, actual); } private static void TestTwoDuplicate() { IList points = new List{new Point(0, 0), new Point(0, 0)}; IList actual = ConvexHull.MakeHull(points); IList expect = new List{new Point(0, 0)}; AssertEquals(expect, actual); } private static void TestTwoHorizontal0() { IList points = new List{new Point(2, 0), new Point(5, 0)}; IList actual = ConvexHull.MakeHull(points); IList expect = points; AssertEquals(expect, actual); } private static void TestTwoHorizontal1() { IList points = new List{new Point(-6, -3), new Point(-8, -3)}; IList actual = ConvexHull.MakeHull(points); IList expect = new List{new Point(-8, -3), new Point(-6, -3)}; AssertEquals(expect, actual); } private static void TestTwoVertical0() { IList points = new List{new Point(1, -4), new Point(1, 4)}; IList actual = ConvexHull.MakeHull(points); IList expect = points; AssertEquals(expect, actual); } private static void TestTwoVertical1() { IList points = new List{new Point(-1, 2), new Point(-1, -3)}; IList actual = ConvexHull.MakeHull(points); IList expect = new List{new Point(-1, -3), new Point(-1, 2)}; AssertEquals(expect, actual); } private static void TestTwoDiagonal0() { IList points = new List{new Point(-2, -3), new Point(2, 0)}; IList actual = ConvexHull.MakeHull(points); IList expect = points; AssertEquals(expect, actual); } private static void TestTwoDiagonal1() { IList points = new List{new Point(-2, 3), new Point(2, 0)}; IList actual = ConvexHull.MakeHull(points); IList expect = points; AssertEquals(expect, actual); } private static void TestRectangle() { IList points = new List{new Point(-3, 2), new Point(1, 2), new Point(1, -4), new Point(-3, -4)}; IList actual = ConvexHull.MakeHull(points); IList expect = new List{new Point(-3, -4), new Point(-3, 2), new Point(1, 2), new Point(1, -4)}; AssertEquals(expect, actual); } /*---- Randomized testing ----*/ private static void TestHorizontalRandomly() { const int TRIALS = 100000; for (int i = 0; i < TRIALS; i++) { int len = rand.Next(30) + 1; IList points = new List(); if (rand.NextDouble() < 0.5) { double y = NextGaussian(); for (int j = 0; j < len; j++) points.Add(new Point(NextGaussian(), y)); } else { int y = rand.Next(20) - 10; for (int j = 0; j < len; j++) points.Add(new Point(rand.Next(30), y)); } IList actual = ConvexHull.MakeHull(points); IList expected = new List(); expected.Add(points.Min()); if (points.Max().CompareTo(expected[0]) != 0) expected.Add(points.Max()); AssertEquals(expected, actual); } } private static void TestVerticalRandomly() { const int TRIALS = 100000; for (int i = 0; i < TRIALS; i++) { int len = rand.Next(30) + 1; IList points = new List(); if (rand.NextDouble() < 0.5) { double x = NextGaussian(); for (int j = 0; j < len; j++) points.Add(new Point(x, NextGaussian())); } else { int x = rand.Next(20) - 10; for (int j = 0; j < len; j++) points.Add(new Point(x, rand.Next(30))); } IList actual = ConvexHull.MakeHull(points); IList expected = new List(); expected.Add(points.Min()); if (points.Max().CompareTo(expected[0]) != 0) expected.Add(points.Max()); AssertEquals(expected, actual); } } private static void TestVsNaiveRandomly() { const int TRIALS = 100000; for (int i = 0; i < TRIALS; i++) { int len = rand.Next(100); IList points = new List(); if (rand.NextDouble() < 0.5) { for (int j = 0; j < len; j++) points.Add(new Point(NextGaussian(), NextGaussian())); } else { for (int j = 0; j < len; j++) points.Add(new Point(rand.Next(10), rand.Next(10))); } IList actual = ConvexHull.MakeHull(points); IList expected = MakeHullNaive(points); AssertEquals(expected, actual); } } private static void TestHullPropertiesRandomly() { const int TRIALS = 100000; for (int i = 0; i < TRIALS; i++) { // Generate random points int len = rand.Next(100); IList points = new List(); if (rand.NextDouble() < 0.5) { for (int j = 0; j < len; j++) points.Add(new Point(NextGaussian(), NextGaussian())); } else { for (int j = 0; j < len; j++) points.Add(new Point(rand.Next(10), rand.Next(10))); } // Compute hull and check properties IList hull = ConvexHull.MakeHull(points); AssertTrue(IsPolygonConvex(hull)); foreach (Point p in points) AssertTrue(IsPointInConvexPolygon(hull, p)); // Add duplicate points and check new hull if (points.Count > 0) { int dupe = rand.Next(10) + 1; for (int j = 0; j < dupe; j++) points.Add(points[rand.Next(points.Count)]); IList nextHull = ConvexHull.MakeHull(points); AssertEquals(hull, nextHull); } } } private static IList MakeHullNaive(IList points) { if (points.Count <= 1) return new List(points); IList result = new List(); // Jarvis march / gift wrapping algorithm Point point = points.Min(); do { result.Add(point); Point next = points[0]; foreach (Point p in 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.CompareTo(result[0]) != 0); return result; } private static bool IsPolygonConvex(IList points) { int signum = 0; for (int i = 0; i + 2 < points.Count; i++) { Point p = points[i + 0]; Point q = points[i + 1]; Point r = points[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 (signum == 0) signum = sign; else if (sign != signum) return false; } return true; } private static bool IsPointInConvexPolygon(IList polygon, Point point) { int signum = 0; for (int i = 0; i < polygon.Count; i++) { Point p = polygon[i]; Point q = polygon[(i + 1) % polygon.Count]; 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 (signum == 0) signum = sign; else if (sign != signum) return false; } return true; } private static void AssertEquals(IList a, IList b) { if (!Enumerable.SequenceEqual(a, b)) throw new SystemException("Assertion error"); } private static void AssertTrue(bool cond) { if (!cond) throw new SystemException("Assertion error"); } private static int Signum(double x) { if (x > 0) return +1; else if (x < 0) return -1; else return 0; } private static double NextGaussian() { return Math.Sqrt(-2 * Math.Log(rand.NextDouble())) * Math.Cos(rand.NextDouble() * Math.PI * 2); } private static Random rand = new Random(); }