View Javadoc
1   package org.djutils.draw.line;
2   
3   import static org.junit.Assert.assertEquals;
4   import static org.junit.Assert.fail;
5   
6   import java.io.IOException;
7   import java.util.ArrayList;
8   import java.util.Arrays;
9   import java.util.Collection;
10  import java.util.Collections;
11  import java.util.LinkedHashMap;
12  import java.util.LinkedHashSet;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Random;
16  
17  import org.djutils.draw.DrawRuntimeException;
18  import org.djutils.draw.Drawable2d;
19  import org.djutils.draw.point.Point2d;
20  import org.junit.Test;
21  
22  /**
23   * ConvexHullTest.java.
24   * <p>
25   * Copyright (c) 2020-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
26   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
27   * </p>
28   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
29   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
30   */
31  public class ConvexHullTest
32  {
33  
34      /**
35       * Create the map of all convex hull implementations.
36       * @return Map&lt;String, ConvexHullImplementation&gt;; the map of all convex hull implementations
37       */
38      public static Map<String, ConvexHullImplementation> getImplementations()
39      {
40          Map<String, ConvexHullImplementation> implementations = new LinkedHashMap<>();
41          implementations.put("Monotone", new ConvexHullImplementation()
42          {
43              @Override
44              public Polygon2d run(final List<Point2d> points) throws NullPointerException, DrawRuntimeException
45              {
46                  return ConvexHull.convexHullMonotone(points);
47              }
48          });
49          implementations.put("Alshamrani", new ConvexHullImplementation()
50          {
51              @Override
52              public Polygon2d run(final List<Point2d> points) throws NullPointerException, DrawRuntimeException
53              {
54                  return ConvexHull.convexHullAlshamrani(points);
55              }
56          });
57          implementations.put("Default", new ConvexHullImplementation()
58          {
59              @Override
60              public Polygon2d run(final List<Point2d> points) throws NullPointerException, DrawRuntimeException
61              {
62                  return ConvexHull.convexHull(points);
63              }
64          });
65          return implementations;
66      }
67  
68      /**
69       * Test a convex hull implementation.
70       */
71      @Test
72      public void testConvexHull()
73      {
74          Map<String, ConvexHullImplementation> implementations = getImplementations();
75          for (String name : implementations.keySet())
76          {
77              ConvexHullImplementation chi = implementations.get(name);
78              try
79              {
80                  chi.run(null);
81                  fail("should have thrown a NullPointerException");
82              }
83              catch (NullPointerException npe)
84              {
85                  // Ignore expected exception
86              }
87  
88              try
89              {
90                  chi.run(new ArrayList<>());
91                  fail("Empty list should have thrown a DrawRuntimeException");
92              }
93              catch (DrawRuntimeException dre)
94              {
95                  // Ignore expected exception
96              }
97  
98              Point2d testPoint = new Point2d(2, 3);
99              List<Point2d> points = Arrays.asList(testPoint);
100             try
101             {
102                 chi.run(points);
103                 fail("List with only one point should have thrown a DrawRuntimeException");
104             }
105             catch (DrawRuntimeException dre)
106             {
107                 // Ignore expected exception
108             }
109             
110             // Verify that the provided array was not modified.
111             assertEquals("points still contains one point", 1, points.size());
112             assertEquals("points still contains testPoint", testPoint, points.get(0));
113             
114             points = new ArrayList<>();
115             points.add(testPoint);
116             points.add(testPoint);
117             try
118             {
119                 chi.run(points);
120                 fail("List with only two identical should have thrown a DrawRuntimeException");
121             }
122             catch (DrawRuntimeException dre)
123             {
124                 // Ignore expected exception
125             }
126             
127             // Verify that the provided array was not modified.
128             assertEquals("points still contains one point", 2, points.size());
129             assertEquals("first points is testPoint", testPoint, points.get(0));
130             assertEquals("second points is testPoint", testPoint, points.get(1));
131         }
132         try
133         {
134             ConvexHull.convexHull(new LinkedHashSet<Drawable2d>());
135             fail("empty collection should have thrown a DrawRuntimeException");
136         }
137         catch (DrawRuntimeException dre)
138         {
139             // Ignore expected exception
140         }
141 
142         try
143         {
144             ConvexHull.convexHull((Collection<Drawable2d>) null);
145             fail("null collection should have thrown a NullPointerException");
146         }
147         catch (NullPointerException npe)
148         {
149             // Ignore expected exception
150         }
151 
152         // Example set from https://rosettacode.org/wiki/Convex_hull#Java
153         List<Point2d> points = Arrays.asList(new Point2d(16, 3), new Point2d(12, 17), new Point2d(0, 6), new Point2d(-4, -6),
154                 new Point2d(16, 6), new Point2d(16, -7), new Point2d(16, -3), new Point2d(17, -4), new Point2d(5, 19),
155                 new Point2d(19, -8), new Point2d(3, 16), new Point2d(12, 13), new Point2d(3, -4), new Point2d(17, 5),
156                 new Point2d(-3, 15), new Point2d(-3, -9), new Point2d(0, 11), new Point2d(-9, -3), new Point2d(-4, -2),
157                 new Point2d(12, 10));
158         Polygon2d expectedResult = new Polygon2d(new Point2d(-9, -3), new Point2d(-3, -9), new Point2d(19, -8),
159                 new Point2d(17, 5), new Point2d(12, 17), new Point2d(5, 19), new Point2d(-3, 15));
160         checkImplementations(implementations, points, expectedResult);
161         Collections.shuffle(points, new Random(123));
162         checkImplementations(implementations, points, expectedResult);
163 
164         assertEquals("convex hull of iterator", expectedResult, ConvexHull.convexHull(points.iterator()));
165 
166         assertEquals("convex hull of one drawable", expectedResult, ConvexHull.convexHull(new PolyLine2d(points)));
167 
168         assertEquals("convex hull of two drawables", expectedResult,
169                 ConvexHull.convexHull(new PolyLine2d(points), new Point2d(1, 2)));
170         assertEquals("convex hull of two drawables", expectedResult,
171                 ConvexHull.convexHull(new Point2d(1, 2), new PolyLine2d(points)));
172 
173         Collection<Drawable2d> collection = new LinkedHashSet<>();
174         collection.add(new PolyLine2d(points));
175         assertEquals("convex hull of collection of one Drawable2d object", expectedResult, ConvexHull.convexHull(collection));
176 
177         collection.add(new Point2d(1, 2));
178         assertEquals("convex hull of collection of two Drawable2d objects", expectedResult, ConvexHull.convexHull(collection));
179 
180         points = new ArrayList<>();
181         for (int x = -1; x <= 1; x++)
182         {
183             for (int y = -2; y <= 2; y++)
184             {
185                 points.add(new Point2d(x, y));
186             }
187         }
188         expectedResult = new Polygon2d(new Point2d(-1, -2), new Point2d(1, -2), new Point2d(1, 2), new Point2d(-1, 2));
189         checkImplementations(implementations, points, expectedResult);
190         Collections.shuffle(points, new Random(234));
191         checkImplementations(implementations, points, expectedResult);
192 
193         points.add(new Point2d(-1.1, 0));
194         points.add(new Point2d(0, -2.1));
195         points.add(new Point2d(1.1, 0));
196         points.add(new Point2d(0, 2.1));
197         expectedResult = new Polygon2d(new Point2d(-1.1, 0), new Point2d(-1, -2), new Point2d(0, -2.1), new Point2d(1, -2),
198                 new Point2d(1.1, 0), new Point2d(1, 2), new Point2d(0, 2.1), new Point2d(-1, 2));
199         checkImplementations(implementations, points, expectedResult);
200         Collections.shuffle(points, new Random(345));
201         checkImplementations(implementations, points, expectedResult);
202 
203         points.clear();
204         double radius = 5000.0 / 64;
205         double centerX = 1.5;
206         double centerY = 10.5;
207         // These for loops should not suffer from rounding errors
208         for (double x = centerX - radius; x <= centerX + radius; x += 1)
209         {
210             for (double y = centerY - radius; y <= centerY + radius; y += 1)
211             {
212                 double distance = Math.hypot(x - centerX, y - centerY);
213                 if (distance <= radius)
214                 {
215                     points.add(new Point2d(x, y));
216                 }
217             }
218         }
219         // It is a bit hard to construct the expected result; we'll use the result of convexHullMonotone as reference because it
220         // is simpler and therefore less likely to contain errors.
221         expectedResult = ConvexHull.convexHullMonotone(new ArrayList<>(points));
222         checkImplementations(implementations, points, expectedResult);
223         Collections.shuffle(points, new Random(456));
224         checkImplementations(implementations, points, expectedResult);
225     }
226 
227     /**
228      * Compare performance.
229      * @param args String[]; the command line arguments (not used)
230      * @throws IOException ...
231      */
232     public static void main(final String[] args) throws IOException
233     {
234         Map<String, ConvexHullImplementation> implementations = getImplementations();
235         List<Point2d> points = new ArrayList<>();
236         double centerX = 1.5;
237         double centerY = 10.5;
238         System.out.println("type return when the profiler is ready");
239         System.in.read();
240         for (double radius = 5000.0 / 64; radius <= 6000; radius *= 2)
241         {
242             // These for loops should not suffer from rounding errors
243             points.clear();
244             for (double x = centerX - radius; x <= centerX + radius; x += 1)
245             {
246                 for (double y = centerY - radius; y <= centerY + radius; y += 1)
247                 {
248                     double distance = Math.hypot(x - centerX, y - centerY);
249                     if (distance <= radius)
250                     {
251                         points.add(new Point2d(x, y));
252                     }
253                 }
254             }
255             System.out.print("radius=" + radius + "; ordered input data\n");
256             for (String name : implementations.keySet())
257             {
258                 ConvexHullImplementation implementation = implementations.get(name);
259                 List<Point2d> workList = new ArrayList<>(points);
260                 System.gc();
261                 long startNanos = System.nanoTime();
262                 implementation.run(workList);
263                 long endNanos = System.nanoTime();
264                 double duration = (endNanos - startNanos) / 1e9;
265                 System.out.println(String.format("%d points %s: %.3f s", points.size(), name, duration));
266             }
267             Collections.shuffle(points, new Random(876));
268             System.out.print("Radius=" + radius + "; scrambled input data\n");
269             for (String name : implementations.keySet())
270             {
271                 ConvexHullImplementation implementation = implementations.get(name);
272                 List<Point2d> workList = new ArrayList<>(points);
273                 System.gc();
274                 long startNanos = System.nanoTime();
275                 implementation.run(workList);
276                 long endNanos = System.nanoTime();
277                 double duration = (endNanos - startNanos) / 1e9;
278                 System.out.println(String.format("%d points %s: %.3f s", points.size(), name, duration));
279             }
280         }
281     }
282 
283     /**
284      * Check that all implementations of convex hull give the expected result.
285      * @param implementations Map&lt;String, ConvexHullImplementation&gt;; the implementations to check
286      * @param in List&lt;Point2d&gt;; the input for the convex hull implementations
287      * @param expectedResult Polygon2d; the expected result
288      */
289     public static void checkImplementations(final Map<String, ConvexHullImplementation> implementations, final List<Point2d> in,
290             final Polygon2d expectedResult)
291     {
292         for (String name : implementations.keySet())
293         {
294             Polygon2d result = implementations.get(name).run(new ArrayList<>(in));
295             if (!result.equals(expectedResult))
296             {
297                 System.err.println("Discrepancy for " + name);
298                 System.err.println("        result: " + result.toPlot());
299                 System.err.println("expectedResult: " + expectedResult.toPlot());
300                 System.err.println("         input: " + in);
301                 implementations.get(name).run(new ArrayList<>(in));
302             }
303             assertEquals(name, expectedResult, result);
304         }
305 
306     }
307 
308     /**
309      * Wrapper for any convex hull implementation.
310      */
311     interface ConvexHullImplementation
312     {
313         /**
314          * Run a particular implementation of the convex hull algorithm.
315          * @param points List&lt;Point2d&gt;; the points for which the convex hull must be constructed
316          * @return Polygon2d; the convex hull of the points
317          * @throws NullPointerException when list is null
318          * @throws DrawRuntimeException when list is empty
319          */
320         Polygon2d run(List<Point2d> points) throws NullPointerException, DrawRuntimeException;
321     }
322 
323 }