package org.djutils.draw.line; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Random; import org.djutils.draw.DrawRuntimeException; import org.djutils.draw.Drawable2d; import org.djutils.draw.point.Point2d; import org.junit.Test; /** * ConvexHullTest.java. *

* Copyright (c) 2020-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See DJUTILS License. *

* @author Alexander Verbraeck * @author Peter Knoppers */ public class ConvexHullTest { /** * Create the map of all convex hull implementations. * @return Map<String, ConvexHullImplementation>; the map of all convex hull implementations */ public static Map getImplementations() { Map implementations = new LinkedHashMap<>(); implementations.put("Monotone", new ConvexHullImplementation() { @Override public Polygon2d run(final List points) throws NullPointerException, DrawRuntimeException { return ConvexHull.convexHullMonotone(points); } }); implementations.put("Alshamrani", new ConvexHullImplementation() { @Override public Polygon2d run(final List points) throws NullPointerException, DrawRuntimeException { return ConvexHull.convexHullAlshamrani(points); } }); implementations.put("Default", new ConvexHullImplementation() { @Override public Polygon2d run(final List points) throws NullPointerException, DrawRuntimeException { return ConvexHull.convexHull(points); } }); return implementations; } /** * Test a convex hull implementation. */ @Test public void testConvexHull() { Map implementations = getImplementations(); for (String name : implementations.keySet()) { ConvexHullImplementation chi = implementations.get(name); try { chi.run(null); fail("should have thrown a NullPointerException"); } catch (NullPointerException npe) { // Ignore expected exception } try { chi.run(new ArrayList<>()); fail("Empty list should have thrown a DrawRuntimeException"); } catch (DrawRuntimeException dre) { // Ignore expected exception } Point2d testPoint = new Point2d(2, 3); List points = Arrays.asList(testPoint); try { chi.run(points); fail("List with only one point should have thrown a DrawRuntimeException"); } catch (DrawRuntimeException dre) { // Ignore expected exception } // Verify that the provided array was not modified. assertEquals("points still contains one point", 1, points.size()); assertEquals("points still contains testPoint", testPoint, points.get(0)); points = new ArrayList<>(); points.add(testPoint); points.add(testPoint); try { chi.run(points); fail("List with only two identical should have thrown a DrawRuntimeException"); } catch (DrawRuntimeException dre) { // Ignore expected exception } // Verify that the provided array was not modified. assertEquals("points still contains one point", 2, points.size()); assertEquals("first points is testPoint", testPoint, points.get(0)); assertEquals("second points is testPoint", testPoint, points.get(1)); } try { ConvexHull.convexHull(new LinkedHashSet()); fail("empty collection should have thrown a DrawRuntimeException"); } catch (DrawRuntimeException dre) { // Ignore expected exception } try { ConvexHull.convexHull((Collection) null); fail("null collection should have thrown a NullPointerException"); } catch (NullPointerException npe) { // Ignore expected exception } // Example set from https://rosettacode.org/wiki/Convex_hull#Java List points = Arrays.asList(new Point2d(16, 3), new Point2d(12, 17), new Point2d(0, 6), new Point2d(-4, -6), new Point2d(16, 6), new Point2d(16, -7), new Point2d(16, -3), new Point2d(17, -4), new Point2d(5, 19), new Point2d(19, -8), new Point2d(3, 16), new Point2d(12, 13), new Point2d(3, -4), new Point2d(17, 5), new Point2d(-3, 15), new Point2d(-3, -9), new Point2d(0, 11), new Point2d(-9, -3), new Point2d(-4, -2), new Point2d(12, 10)); Polygon2d expectedResult = new Polygon2d(new Point2d(-9, -3), new Point2d(-3, -9), new Point2d(19, -8), new Point2d(17, 5), new Point2d(12, 17), new Point2d(5, 19), new Point2d(-3, 15)); checkImplementations(implementations, points, expectedResult); Collections.shuffle(points, new Random(123)); checkImplementations(implementations, points, expectedResult); assertEquals("convex hull of iterator", expectedResult, ConvexHull.convexHull(points.iterator())); assertEquals("convex hull of one drawable", expectedResult, ConvexHull.convexHull(new PolyLine2d(points))); assertEquals("convex hull of two drawables", expectedResult, ConvexHull.convexHull(new PolyLine2d(points), new Point2d(1, 2))); assertEquals("convex hull of two drawables", expectedResult, ConvexHull.convexHull(new Point2d(1, 2), new PolyLine2d(points))); Collection collection = new LinkedHashSet<>(); collection.add(new PolyLine2d(points)); assertEquals("convex hull of collection of one Drawable2d object", expectedResult, ConvexHull.convexHull(collection)); collection.add(new Point2d(1, 2)); assertEquals("convex hull of collection of two Drawable2d objects", expectedResult, ConvexHull.convexHull(collection)); points = new ArrayList<>(); for (int x = -1; x <= 1; x++) { for (int y = -2; y <= 2; y++) { points.add(new Point2d(x, y)); } } expectedResult = new Polygon2d(new Point2d(-1, -2), new Point2d(1, -2), new Point2d(1, 2), new Point2d(-1, 2)); checkImplementations(implementations, points, expectedResult); Collections.shuffle(points, new Random(234)); checkImplementations(implementations, points, expectedResult); points.add(new Point2d(-1.1, 0)); points.add(new Point2d(0, -2.1)); points.add(new Point2d(1.1, 0)); points.add(new Point2d(0, 2.1)); expectedResult = new Polygon2d(new Point2d(-1.1, 0), new Point2d(-1, -2), new Point2d(0, -2.1), new Point2d(1, -2), new Point2d(1.1, 0), new Point2d(1, 2), new Point2d(0, 2.1), new Point2d(-1, 2)); checkImplementations(implementations, points, expectedResult); Collections.shuffle(points, new Random(345)); checkImplementations(implementations, points, expectedResult); points.clear(); double radius = 5000.0 / 64; double centerX = 1.5; double centerY = 10.5; // These for loops should not suffer from rounding errors for (double x = centerX - radius; x <= centerX + radius; x += 1) { for (double y = centerY - radius; y <= centerY + radius; y += 1) { double distance = Math.hypot(x - centerX, y - centerY); if (distance <= radius) { points.add(new Point2d(x, y)); } } } // It is a bit hard to construct the expected result; we'll use the result of convexHullMonotone as reference because it // is simpler and therefore less likely to contain errors. expectedResult = ConvexHull.convexHullMonotone(new ArrayList<>(points)); checkImplementations(implementations, points, expectedResult); Collections.shuffle(points, new Random(456)); checkImplementations(implementations, points, expectedResult); } /** * Compare performance. * @param args String[]; the command line arguments (not used) * @throws IOException ... */ public static void main(final String[] args) throws IOException { Map implementations = getImplementations(); List points = new ArrayList<>(); double centerX = 1.5; double centerY = 10.5; System.out.println("type return when the profiler is ready"); System.in.read(); for (double radius = 5000.0 / 64; radius <= 6000; radius *= 2) { // These for loops should not suffer from rounding errors points.clear(); for (double x = centerX - radius; x <= centerX + radius; x += 1) { for (double y = centerY - radius; y <= centerY + radius; y += 1) { double distance = Math.hypot(x - centerX, y - centerY); if (distance <= radius) { points.add(new Point2d(x, y)); } } } System.out.print("radius=" + radius + "; ordered input data\n"); for (String name : implementations.keySet()) { ConvexHullImplementation implementation = implementations.get(name); List workList = new ArrayList<>(points); System.gc(); long startNanos = System.nanoTime(); implementation.run(workList); long endNanos = System.nanoTime(); double duration = (endNanos - startNanos) / 1e9; System.out.println(String.format("%d points %s: %.3f s", points.size(), name, duration)); } Collections.shuffle(points, new Random(876)); System.out.print("Radius=" + radius + "; scrambled input data\n"); for (String name : implementations.keySet()) { ConvexHullImplementation implementation = implementations.get(name); List workList = new ArrayList<>(points); System.gc(); long startNanos = System.nanoTime(); implementation.run(workList); long endNanos = System.nanoTime(); double duration = (endNanos - startNanos) / 1e9; System.out.println(String.format("%d points %s: %.3f s", points.size(), name, duration)); } } } /** * Check that all implementations of convex hull give the expected result. * @param implementations Map<String, ConvexHullImplementation>; the implementations to check * @param in List<Point2d>; the input for the convex hull implementations * @param expectedResult Polygon2d; the expected result */ public static void checkImplementations(final Map implementations, final List in, final Polygon2d expectedResult) { for (String name : implementations.keySet()) { Polygon2d result = implementations.get(name).run(new ArrayList<>(in)); if (!result.equals(expectedResult)) { System.err.println("Discrepancy for " + name); System.err.println(" result: " + result.toPlot()); System.err.println("expectedResult: " + expectedResult.toPlot()); System.err.println(" input: " + in); implementations.get(name).run(new ArrayList<>(in)); } assertEquals(name, expectedResult, result); } } /** * Wrapper for any convex hull implementation. */ interface ConvexHullImplementation { /** * Run a particular implementation of the convex hull algorithm. * @param points List<Point2d>; the points for which the convex hull must be constructed * @return Polygon2d; the convex hull of the points * @throws NullPointerException when list is null * @throws DrawRuntimeException when list is empty */ Polygon2d run(List points) throws NullPointerException, DrawRuntimeException; } }