/*
* Convex hull algorithm - Test suite (Java)
*
* 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 .
*/
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import org.junit.Test;
public final class ConvexHullTest {
/*---- Fixed test vectors ----*/
@Test public void testEmpty() {
List points = Collections.emptyList();
List actual = ConvexHull.makeHull(points);
List expect = Collections.emptyList();
assertEquals(expect, actual);
}
@Test public void testOne() {
List points = Arrays.asList(new Point(3, 1));
List actual = ConvexHull.makeHull(points);
List expect = points;
assertEquals(expect, actual);
}
@Test public void testTwoDuplicate() {
List points = Arrays.asList(new Point(0, 0), new Point(0, 0));
List actual = ConvexHull.makeHull(points);
List expect = Arrays.asList(new Point(0, 0));
assertEquals(expect, actual);
}
@Test public void testTwoHorizontal0() {
List points = Arrays.asList(new Point(2, 0), new Point(5, 0));
List actual = ConvexHull.makeHull(points);
List expect = points;
assertEquals(expect, actual);
}
@Test public void testTwoHorizontal1() {
List points = Arrays.asList(new Point(-6, -3), new Point(-8, -3));
List actual = ConvexHull.makeHull(points);
List expect = Arrays.asList(new Point(-8, -3), new Point(-6, -3));
assertEquals(expect, actual);
}
@Test public void testTwoVertical0() {
List points = Arrays.asList(new Point(1, -4), new Point(1, 4));
List actual = ConvexHull.makeHull(points);
List expect = points;
assertEquals(expect, actual);
}
@Test public void testTwoVertical1() {
List points = Arrays.asList(new Point(-1, 2), new Point(-1, -3));
List actual = ConvexHull.makeHull(points);
List expect = Arrays.asList(new Point(-1, -3), new Point(-1, 2));
assertEquals(expect, actual);
}
@Test public void testTwoDiagonal0() {
List points = Arrays.asList(new Point(-2, -3), new Point(2, 0));
List actual = ConvexHull.makeHull(points);
List expect = points;
assertEquals(expect, actual);
}
@Test public void testTwoDiagonal1() {
List points = Arrays.asList(new Point(-2, 3), new Point(2, 0));
List actual = ConvexHull.makeHull(points);
List expect = points;
assertEquals(expect, actual);
}
@Test public void testRectangle() {
List points = Arrays.asList(new Point(-3, 2), new Point(1, 2), new Point(1, -4), new Point(-3, -4));
List actual = ConvexHull.makeHull(points);
List expect = Arrays.asList(new Point(-3, -4), new Point(-3, 2), new Point(1, 2), new Point(1, -4));
assertEquals(expect, actual);
}
/*---- Randomized testing ----*/
@Test public void testHorizontalRandomly() {
final int TRIALS = 100000;
for (int i = 0; i < TRIALS; i++) {
int len = rand.nextInt(30) + 1;
List points = new ArrayList<>();
if (rand.nextBoolean()) {
double y = rand.nextGaussian();
for (int j = 0; j < len; j++)
points.add(new Point(rand.nextGaussian(), y));
} else {
int y = rand.nextInt(20) - 10;
for (int j = 0; j < len; j++)
points.add(new Point(rand.nextInt(30), y));
}
List actual = ConvexHull.makeHull(points);
List expected = new ArrayList<>();
expected.add(Collections.min(points));
if (!Collections.max(points).equals(expected.get(0)))
expected.add(Collections.max(points));
assertEquals(expected, actual);
}
}
@Test public void testVerticalRandomly() {
final int TRIALS = 100000;
for (int i = 0; i < TRIALS; i++) {
int len = rand.nextInt(30) + 1;
List points = new ArrayList<>();
if (rand.nextBoolean()) {
double x = rand.nextGaussian();
for (int j = 0; j < len; j++)
points.add(new Point(x, rand.nextGaussian()));
} else {
int x = rand.nextInt(20) - 10;
for (int j = 0; j < len; j++)
points.add(new Point(x, rand.nextInt(30)));
}
List actual = ConvexHull.makeHull(points);
List expected = new ArrayList<>();
expected.add(Collections.min(points));
if (!Collections.max(points).equals(expected.get(0)))
expected.add(Collections.max(points));
assertEquals(expected, actual);
}
}
@Test public void testVsNaiveRandomly() {
final int TRIALS = 100000;
for (int i = 0; i < TRIALS; i++) {
int len = rand.nextInt(100);
List points = new ArrayList<>();
if (rand.nextBoolean()) {
for (int j = 0; j < len; j++)
points.add(new Point(rand.nextGaussian(), rand.nextGaussian()));
} else {
for (int j = 0; j < len; j++)
points.add(new Point(rand.nextInt(10), rand.nextInt(10)));
}
List actual = ConvexHull.makeHull(points);
List expected = makeHullNaive(points);
assertEquals(expected, actual);
}
}
@Test public void testHullPropertiesRandomly() {
final int TRIALS = 100000;
for (int i = 0; i < TRIALS; i++) {
// Generate random points
int len = rand.nextInt(100);
List points = new ArrayList<>();
if (rand.nextBoolean()) {
for (int j = 0; j < len; j++)
points.add(new Point(rand.nextGaussian(), rand.nextGaussian()));
} else {
for (int j = 0; j < len; j++)
points.add(new Point(rand.nextInt(10), rand.nextInt(10)));
}
// Compute hull and check properties
List hull = ConvexHull.makeHull(points);
assertTrue(isPolygonConvex(hull));
for (Point p : points)
assertTrue(isPointInConvexPolygon(hull, p));
// Add duplicate points and check new hull
if (!points.isEmpty()) {
int dupe = rand.nextInt(10) + 1;
for (int j = 0; j < dupe; j++)
points.add(points.get(rand.nextInt(points.size())));
List nextHull = ConvexHull.makeHull(points);
assertEquals(hull, nextHull);
}
}
}
private static List makeHullNaive(List points) {
if (points.size() <= 1)
return new ArrayList<>(points);
List result = new ArrayList<>();
// Jarvis march / gift wrapping algorithm
Point point = Collections.min(points);
do {
result.add(point);
Point next = points.get(0);
for (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.equals(result.get(0)));
return result;
}
private static boolean isPolygonConvex(List points) {
int signum = 0;
for (int i = 0; i + 2 < points.size(); i++) {
Point p = points.get(i + 0);
Point q = points.get(i + 1);
Point r = points.get(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 boolean isPointInConvexPolygon(List polygon, Point point) {
int signum = 0;
for (int i = 0; i < polygon.size(); i++) {
Point p = polygon.get(i);
Point q = polygon.get((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 (signum == 0)
signum = sign;
else if (sign != signum)
return false;
}
return true;
}
private static int signum(double x) {
if (x > 0)
return +1;
else if (x < 0)
return -1;
else
return 0;
}
private static final Random rand = new Random();
}