View Javadoc
1   package org.djutils.draw.line;
2   
3   import static org.junit.Assert.assertEquals;
4   import static org.junit.Assert.assertFalse;
5   import static org.junit.Assert.assertTrue;
6   import static org.junit.Assert.fail;
7   
8   import java.awt.geom.Path2D;
9   import java.awt.geom.PathIterator;
10  import java.util.ArrayList;
11  import java.util.Arrays;
12  import java.util.List;
13  
14  import org.djutils.draw.DrawRuntimeException;
15  import org.djutils.draw.Transform2d;
16  import org.djutils.draw.bounds.Bounds2d;
17  import org.djutils.draw.point.Point2d;
18  import org.junit.Test;
19  
20  /**
21   * Polygon2dTest.java.
22   * <p>
23   * Copyright (c) 2020-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
24   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
25   * </p>
26   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
27   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
28   */
29  public class Polygon2dTest
30  {
31  
32      /**
33       * Test the constructors.
34       */
35      @Test
36      public void testConstructors()
37      {
38          double[] x = new double[] {1, 3, 5};
39          double[] y = new double[] {2, 1, 10};
40          double actualSurface = ((x[0] - x[2]) * (y[1] - y[0]) - (x[0] - x[1]) * (y[2] - y[0])) / 2;
41  
42          Polygon2d polygon = new Polygon2d(x, y);
43          checkPolygon("constructed from arrays", x, y, polygon, actualSurface, true);
44          Polygon2d reversed = polygon.reverse();
45          assertEquals("surface of reversed polygon", -actualSurface, reversed.surface(), Math.ulp(-actualSurface));
46          assertTrue("reversed polygon is also convex", reversed.isConvex());
47  
48          x = new double[] {1, 3, 5, 1};
49          y = new double[] {2, 1, 10, 2};
50          polygon = new Polygon2d(x, y); // Last point is duplicate of first point; should be handled gracefully
51          assertTrue("toString returns something descriptive", polygon.toString().startsWith("Polygon2d"));
52          assertTrue("toString can suppress the class name", polygon.toString().indexOf(polygon.toString(true)) > 0);
53          checkPolygon("constructed from arrays", x, y, polygon, actualSurface, true);
54          assertEquals("surface of reversed polygon", -actualSurface, polygon.reverse().surface(), Math.ulp(-actualSurface));
55          Polygon2d otherPolygon = new Polygon2d(polygon.get(0), polygon.get(1), polygon.get(2), polygon.get(0));
56          assertEquals("polygon constructed from all points of existing polygon with first point duplicated at end is equal "
57                  + "to original", polygon, otherPolygon);
58          // Make a Polygon2d from Point2d where last point differs from first only in y
59          new Polygon2d(polygon.get(0), polygon.get(1), polygon.get(2), new Point2d(polygon.get(0).x, 123));
60  
61          x = new double[] {1, 3, 1}; // x coordinate of last point matches that of first
62          y = new double[] {2, 1, 10}; // not true for y coordinates
63          polygon = new Polygon2d(x, y);
64          // System.out.println(polygon);
65          actualSurface = ((x[0] - x[2]) * (y[1] - y[0]) - (x[0] - x[1]) * (y[2] - y[0])) / 2;
66          checkPolygon("constructed from arrays with first and last x equal", x, y, polygon, actualSurface, true);
67  
68          x = new double[] {1, 3, 5, 3};
69          y = new double[] {2, 2, 10, 10};
70          actualSurface = 2 * 8; // Parallelogram surface with two sides parallel to X-axis is easy
71          polygon = new Polygon2d(x, y);
72          checkPolygon("constructed from arrays", x, y, polygon, actualSurface, true);
73          assertEquals("surface of reversed polygon", -actualSurface, polygon.reverse().surface(), Math.ulp(-actualSurface));
74          // convert the points of polygon to an array of Point2d
75          List<Point2d> list = new ArrayList<>();
76          polygon.getPoints().forEachRemaining(list::add);
77          otherPolygon = new Polygon2d(list);
78          assertEquals("Polygon created from polygon points is equal to original polygon", polygon, otherPolygon);
79          otherPolygon = new Polygon2d(list.get(0), list.get(1), list.get(2), list.get(3));
80          assertEquals("Polygon created from all four points of existing polygon is equal to original", polygon, otherPolygon);
81  
82          Point2d[] pointArray = list.toArray(new Point2d[0]);
83          otherPolygon = new Polygon2d(pointArray);
84          assertEquals("Polygon created from array of points of existing polygon is equal to original", polygon, otherPolygon);
85  
86          list.add(list.get(0));
87          otherPolygon = new Polygon2d(list.iterator());
88          assertEquals("Polygon created from polygon points and duplicate of first point at end is equal to original polygon",
89                  polygon, otherPolygon);
90  
91          otherPolygon = new Polygon2d(list);
92          assertEquals("Polygon created from polygon points and duplicate of first point at end is equal to original polygon",
93                  polygon, otherPolygon);
94          // Add a point that only differs in y
95          list.add(new Point2d(list.get(0).x, 123));
96          new Polygon2d(list.iterator());
97          list.add(list.get(0));
98          new Polygon2d(list.iterator());
99  
100         // Make last TWO points duplicate of first point
101         list.add(list.get(0));
102         try
103         {
104             new Polygon2d(list);
105             fail("last two points equal to first point should have thrown a DrawRuntimeException");
106         }
107         catch (DrawRuntimeException dre)
108         {
109             // Ignore expected exception
110         }
111 
112         // Non convex polygon with unneeded points in horizontal and vertical side
113         x = new double[] {0, 5, 10, 5, 10, 0, 0};
114         y = new double[] {0, 0, 0, 5, 10, 10, 5};
115         polygon = new Polygon2d(x, y);
116         checkPolygon("non convex polygon", x, y, polygon, 100 - 25, false);
117         assertFalse("reversed non-convex polygon is also non-convex", polygon.reverse().isConvex());
118 
119         try
120         {
121             polygon.getSegment(-1);
122             fail("Negative index should have thrown a DrawRuntimeException");
123         }
124         catch (DrawRuntimeException dre)
125         {
126             // Ignore expected exception
127         }
128 
129         try
130         {
131             polygon.getSegment(polygon.size());
132             fail("index equal to size (or more) should have thrown a DrawRuntimeException");
133         }
134         catch (DrawRuntimeException dre)
135         {
136             // Ignore expected exception
137         }
138 
139         try
140         {
141             new Polygon2d(new double[] {1, 2, 3}, new double[] {1, 2, 3, 4});
142             fail("unequal length of coordinate array should have thrown a DrawRuntimeException");
143         }
144         catch (DrawRuntimeException dre)
145         {
146             // Ignore expected exception
147         }
148 
149         try
150         {
151             new Polygon2d(new double[] {1, 2, 3, 4}, new double[] {1, 2, 3});
152             fail("unequal length of coordinate array should have thrown a DrawRuntimeException");
153         }
154         catch (DrawRuntimeException dre)
155         {
156             // Ignore expected exception
157         }
158 
159         try
160         {
161             new Polygon2d(null, new double[] {1, 2, 3});
162             fail("null for x array hould have thrown a NullPointerException");
163         }
164         catch (NullPointerException npe)
165         {
166             // Ignore expected exception
167         }
168 
169         try
170         {
171             new Polygon2d(new double[] {1, 2, 3}, null);
172             fail("null for x array hould have thrown a NullPointerException");
173         }
174         catch (NullPointerException npe)
175         {
176             // Ignore expected exception
177         }
178 
179         try
180         {
181             new Polygon2d(new double[] {1}, new double[] {1});
182             fail("too short coordinate array should have thrown a DrawRuntimeException");
183         }
184         catch (DrawRuntimeException dre)
185         {
186             // Ignore expected exception
187         }
188 
189         try
190         {
191             new Polygon2d(new Point2d(1, 2), new Point2d(1, 2), new Point2d[] {});
192             fail("too short coordinate array should have thrown a DrawRuntimeException");
193         }
194         catch (DrawRuntimeException dre)
195         {
196             // Ignore expected exception
197         }
198 
199         try
200         {
201             new Polygon2d(new Point2d(1, 2), new Point2d(1, 2), (Point2d[]) null);
202             fail("too short coordinate array should have thrown a DrawRuntimeException");
203         }
204         catch (DrawRuntimeException dre)
205         {
206             // Ignore expected exception
207         }
208 
209         try
210         {
211             new Polygon2d(new Point2d(1, 2), new Point2d(3, 2), new Point2d[] {new Point2d(1, 2), new Point2d(1, 2)});
212             fail("two identical points at end, matching first point should have thrown a DrawRuntimeException");
213         }
214         catch (DrawRuntimeException dre)
215         {
216             // Ignore expected exception
217         }
218 
219         list.clear();
220         list.add(new Point2d(1, 2));
221         try
222         {
223             new Polygon2d(list);
224             fail("too short list should have thrown a DrawRuntimeException");
225         }
226         catch (DrawRuntimeException dre)
227         {
228             // Ignore expected exception
229         }
230 
231     }
232 
233     /**
234      * Test the filtering constructors.
235      */
236     @Test
237     public void filterTest()
238     {
239         Point2d[] points = new Point2d[] {new Point2d(1, 2), new Point2d(1, 2), new Point2d(4, 5)};
240         try
241         {
242             new Polygon2d(false, points);
243             fail("duplicate point should have thrown a DrawRuntimeException");
244         }
245         catch (DrawRuntimeException dre)
246         {
247             // Ignore expected exception
248         }
249 
250         assertEquals("After filtering; there are two points left", 2, new Polygon2d(true, points).size());
251 
252         List<Point2d> list = Arrays.asList(points);
253         try
254         {
255             new Polygon2d(false, list);
256             fail("duplicate point should have thrown a DrawRuntimeException");
257         }
258         catch (DrawRuntimeException dre)
259         {
260             // Ignore expected exception
261         }
262 
263         assertEquals("After filtering; there are two points left", 2, new Polygon2d(true, list).size());
264     }
265 
266     /**
267      * Verify the various properties of a Polygon2d.
268      * @param where String; description of the test
269      * @param x double[]; the expected x coordinates
270      * @param y double[]; the expected y coordinates
271      * @param polygon Polygon2d; the Polygon2d
272      * @param expectedSurface double; the expected surface of the polygon
273      * @param isConvex boolean; the expected value returned by the isConvex method
274      */
275     private void checkPolygon(final String where, final double[] x, final double[] y, final Polygon2d polygon,
276             final double expectedSurface, final boolean isConvex)
277     {
278         double cumulativeLength = 0;
279         for (int index = 0; index < polygon.size(); index++)
280         {
281             assertEquals(where + " x[index]", x[index], polygon.getX(index), Math.ulp(x[index]));
282             assertEquals(where + " y[index]", y[index], polygon.getY(index), Math.ulp(y[index]));
283             LineSegment2d segment = polygon.getSegment(index);
284             assertEquals(where + " segment start x", x[index], segment.startX, Math.ulp(x[index]));
285             assertEquals(where + " segment start y", y[index], segment.startY, Math.ulp(y[index]));
286             int wrappedIndex = (index + 1) % polygon.size();
287             assertEquals(where + " segment end x", x[wrappedIndex], segment.endX, Math.ulp(x[wrappedIndex]));
288             assertEquals(where + " segment end y", y[wrappedIndex], segment.endY, Math.ulp(y[wrappedIndex]));
289             cumulativeLength += segment.getLength();
290         }
291         assertEquals(where + " surface", expectedSurface, polygon.surface(), Math.ulp(expectedSurface));
292         assertEquals(where + " circumference", cumulativeLength, polygon.getLength(),
293                 polygon.size() * Math.ulp(cumulativeLength));
294         assertEquals(where + " is convex?", isConvex, polygon.isConvex());
295     }
296 
297     /**
298      * Test the contains method for a Point2D.
299      */
300     @Test
301     public void containsPointTest()
302     {
303         // Parallelogram that nowhere crosses integer coordinates; so there is a clear result for all integer coordinates
304         Polygon2d polygon = new Polygon2d(new double[] {4.8, 10.2, 15.2, 9.8}, new double[] {-10.1, -10.1, 0.1, 0.1});
305         // System.out.print(polygon.toPlot() + " c1,0,0");
306         for (int x = 0; x < 20; x += 1)
307         {
308             for (int y = -15; y < 5; y++)
309             {
310                 boolean expected = pointInParallelogram(x, y);
311                 // System.out
312                 // .println(String.format("%s M%.1f,%.1f L%.1f,%.1f M%.1f,%.1f L%.1f,%.1f", expected ? "c1,0,0" : "c0,0,0",
313                 // x - 0.1, (double) y, x + 0.1, (double) y, (double) x, y - 0.1, (double) x, y + 0.1));
314                 assertEquals("contains", expected, polygon.contains(x, y));
315                 assertEquals("contains", expected, polygon.contains(new Point2d(x, y)));
316             }
317         }
318     }
319 
320     /**
321      * Test the contains method for a Bounds2d.
322      */
323     @Test
324     public void containsBoundsTest()
325     {
326         // Parallelogram that nowhere crosses integer coordinates; so there is a clear result for all integer coordinates
327         Polygon2d polygon = new Polygon2d(new double[] {4.8, 10.2, 15.2, 9.8}, new double[] {-10.1, -10.1, 0.1, 0.1});
328         // System.out.print(polygon.toPlot() + " c1,0,0");
329         for (int x = 0; x < 20; x += 1)
330         {
331             for (int y = -15; y < 5; y++)
332             {
333                 for (int dX = 0; dX < 4; dX++)
334                 {
335                     for (int dY = 0; dY < 4; dY++)
336                     {
337                         boolean expected = dX > 0 && dY > 0 && pointInParallelogram(x, y) && pointInParallelogram(x + dX, y)
338                                 && pointInParallelogram(x, y + dY) && pointInParallelogram(x + dX, y + dY);
339                         Bounds2d bounds = new Bounds2d(new Point2d(x, y), new Point2d(x + dX, y + dY));
340                         assertEquals("contains", expected, polygon.contains(bounds));
341                     }
342                 }
343             }
344         }
345     }
346 
347     /**
348      * Construct a list of Point2d spread out regularly over a circle.
349      * @param centerX double; center X of the circle
350      * @param centerY double; center Y of the circle
351      * @param radius double; radius of the circle
352      * @param size int; number of points in the polygon
353      * @return List&lt;OTSPoin3D&gt;; the points that lie on a regular polygon
354      */
355     private List<Point2d> makePolygon(final double centerX, final double centerY, final double radius, final int size)
356     {
357         List<Point2d> points = new ArrayList<>(size);
358         for (int i = 0; i < size; i++)
359         {
360             double angle = Math.PI * 2 * i / size;
361             points.add(new Point2d(centerX + radius * Math.cos(angle), centerY + radius * Math.sin(angle)));
362         }
363         return points;
364     }
365 
366     /**
367      * Test the intersects intersects method.
368      */
369     @Test
370     public final void testIntersects()
371     {
372         double radius = 10;
373         double cx = 5;
374         double cy = -5;
375         Polygon2d reference = new Polygon2d(makePolygon(cx, cy, radius, 18));
376         for (int dx = -20; dx <= 20; dx++)
377         {
378             for (int dy = -20; dy <= 20; dy++)
379             {
380                 boolean hit = true;
381                 double distance = Math.sqrt(dx * dx + dy * dy);
382                 double radius2 = 2;
383                 if (distance > radius + radius2)
384                 {
385                     hit = false;
386                 }
387                 else if (distance > radius + radius2 - 0.1)
388                 {
389                     continue; // too close to be sure
390                 }
391                 Polygon2d other = new Polygon2d(makePolygon(cx + dx, cy + dy, radius2, 16));
392                 if (hit)
393                 {
394                     assertTrue("shapes hit", reference.intersects(other));
395                 }
396                 else
397                 {
398                     assertFalse("shapes do not hit", reference.intersects(other));
399                 }
400             }
401         }
402         reference = new Polygon2d(new Point2d[] {new Point2d(0, 0), new Point2d(10, 0), new Point2d(10, 10)});
403         // Make shapes that overlap along the X axis
404         for (int dx = -20; dx <= 20; dx++)
405         {
406             Polygon2d other = new Polygon2d(new Point2d[] {new Point2d(dx, 0), new Point2d(dx + 5, 0), new Point2d(dx, -20)});
407             boolean hit = dx >= -5 && dx <= 10;
408             // System.out.println("hit="+hit+"\treference: " + reference + "\tother: "+ other);
409             if (hit)
410             {
411                 assertTrue("shapes hit", reference.intersects(other));
412             }
413             else
414             {
415                 assertFalse("shapes do not hit", reference.intersects(other));
416             }
417         }
418         // Make shapes that overlap along the Y axis
419         for (int dy = -20; dy <= 20; dy++)
420         {
421             Polygon2d other = new Polygon2d(new Point2d[] {new Point2d(20, dy), new Point2d(10, dy), new Point2d(10, dy + 10)});
422             boolean hit = dy >= -10 && dy <= 10;
423             if (hit)
424             {
425                 assertTrue("shapes hit", reference.intersects(other));
426             }
427             else
428             {
429                 assertFalse("shapes do not hit", reference.intersects(other));
430             }
431         }
432         // Make vertical and horizontal box
433         Polygon2d vertical = new Polygon2d(new Point2d[] {new Point2d(-1, -10), new Point2d(1, -10), new Point2d(1, 10),
434                 new Point2d(-1, 10), new Point2d(-1, -10)});
435         Polygon2d horizontal = new Polygon2d(new Point2d[] {new Point2d(-10, -1), new Point2d(10, -1), new Point2d(10, 1),
436                 new Point2d(-10, 1), new Point2d(-10, -1)});
437         assertTrue("shapes hit", vertical.intersects(horizontal));
438         // Test all cases of co-linear edges
439         Polygon2d one = new Polygon2d(new Point2d(3, 4), new Point2d(4, 3), new Point2d(4, -3), new Point2d(3, -4),
440                 new Point2d(-3, -4), new Point2d(-4, -3), new Point2d(-4, 3), new Point2d(-3, 4));
441         Polygon2d two = new Polygon2d(new Transform2d().translate(8,0).transform(one.getPoints()));
442         assertTrue("shapes hit", one.intersects(two));
443         two = new Polygon2d(new Transform2d().translate(8,2).transform(one.getPoints()));
444         assertTrue("shapes hit", one.intersects(two));
445         two = new Polygon2d(new Transform2d().translate(8,6).transform(one.getPoints()));
446         assertTrue("shapes hit", one.intersects(two));
447         two = new Polygon2d(new Transform2d().translate(7.75,6.25).transform(one.getPoints()));
448         assertTrue("shapes hit", one.intersects(two));
449         two = new Polygon2d(new Transform2d().translate(7,7).transform(one.getPoints()));
450         assertTrue("shapes hit", one.intersects(two));
451         
452     }
453 
454     /**
455      * Test code used in the contains tests. Only works for the parallelogram that is used in those tests.
456      * @param x int; the X coordinate
457      * @param y int; the Y coordinate
458      * @return boolean; true if (x,y) is inside the parallelogram; false if (x,y) is not inside the parallelogram
459      */
460     private boolean pointInParallelogram(final int x, final int y)
461     {
462         return y <= 0 && y >= -10 && x >= 10 + y * 0.5 && x <= 15 + y * 0.5;
463     }
464 
465     /**
466      * Test the debugging output methods.
467      */
468     @Test
469     public void testExports()
470     {
471         Point2d[] points =
472                 new Point2d[] {new Point2d(123.456, 345.678), new Point2d(234.567, 456.789), new Point2d(-12.345, -34.567)};
473         Polygon2d pl = new Polygon2d(points);
474         String[] out = pl.toExcel().split("\\n");
475         assertEquals("Excel output consists of one line per point plus one", points.length + 1, out.length);
476         for (int index = 0; index <= points.length; index++)
477         {
478             String[] fields = out[index].split("\\t");
479             assertEquals("each line consists of two fields", 2, fields.length);
480             try
481             {
482                 double x = Double.parseDouble(fields[0].trim());
483                 assertEquals("x matches", points[index % pl.size()].x, x, 0.001);
484             }
485             catch (NumberFormatException nfe)
486             {
487                 fail("First field " + fields[0] + " does not parse as a double");
488             }
489             try
490             {
491                 double y = Double.parseDouble(fields[1].trim());
492                 assertEquals("y matches", points[index % pl.size()].y, y, 0.001);
493             }
494             catch (NumberFormatException nfe)
495             {
496                 fail("Second field " + fields[1] + " does not parse as a double");
497             }
498         }
499 
500         out = pl.toPlot().split(" L");
501         assertEquals("Plotter output consists of one coordinate pair per point plus one", points.length + 1, out.length);
502         for (int index = 0; index < points.length; index++)
503         {
504             String[] fields = out[index].split(",");
505             assertEquals("each line consists of two fields", 2, fields.length);
506             if (index == 0)
507             {
508                 assertTrue(fields[0].startsWith("M"));
509                 fields[0] = fields[0].substring(1);
510             }
511             try
512             {
513                 double x = Double.parseDouble(fields[0].trim());
514                 assertEquals("x matches", points[index % pl.size()].x, x, 0.001);
515             }
516             catch (NumberFormatException nfe)
517             {
518                 fail("First field " + fields[0] + " does not parse as a double");
519             }
520             try
521             {
522                 double y = Double.parseDouble(fields[1].trim());
523                 assertEquals("y matches", points[index % pl.size()].y, y, 0.001);
524             }
525             catch (NumberFormatException nfe)
526             {
527                 fail("Second field " + fields[1] + " does not parse as a double");
528             }
529         }
530 
531         Path2D path = pl.toPath2D();
532         int index = 0;
533         for (PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next())
534         {
535             double[] p = new double[6];
536             int segType = pi.currentSegment(p);
537             if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
538             {
539                 if (index == 0)
540                 {
541                     assertEquals("First segment must be move", PathIterator.SEG_MOVETO, segType);
542                 }
543                 else
544                 {
545                     assertEquals("Additional segments are line segments", PathIterator.SEG_LINETO, segType);
546                 }
547                 assertEquals("X coordinate", points[index].x, p[0], 0.00001);
548                 assertEquals("Y coordinate", points[index].y, p[1], 0.00001);
549             }
550             else if (index == points.length)
551             {
552                 assertEquals("Last segment must be close", PathIterator.SEG_CLOSE, segType);
553             }
554             else
555             {
556                 fail("Unexpected segment type");
557             }
558             index++;
559         }
560     }
561 
562 }