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
22
23
24
25
26
27
28
29 public class Polygon2dTest
30 {
31
32
33
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);
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
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};
62 y = new double[] {2, 1, 10};
63 polygon = new Polygon2d(x, y);
64
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;
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
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
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
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
110 }
111
112
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
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
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
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
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
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
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
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
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
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
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
229 }
230
231 }
232
233
234
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
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
261 }
262
263 assertEquals("After filtering; there are two points left", 2, new Polygon2d(true, list).size());
264 }
265
266
267
268
269
270
271
272
273
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
299
300 @Test
301 public void containsPointTest()
302 {
303
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
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
312
313
314 assertEquals("contains", expected, polygon.contains(x, y));
315 assertEquals("contains", expected, polygon.contains(new Point2d(x, y)));
316 }
317 }
318 }
319
320
321
322
323 @Test
324 public void containsBoundsTest()
325 {
326
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
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
349
350
351
352
353
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
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;
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
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
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
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
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
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
456
457
458
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
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 }