View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.euclidean.twod;
18  
19  import org.apache.commons.geometry.core.GeometryTestUtils;
20  import org.apache.commons.geometry.core.RegionLocation;
21  import org.apache.commons.geometry.core.partitioning.Split;
22  import org.apache.commons.geometry.core.partitioning.SplitLocation;
23  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
24  import org.apache.commons.geometry.euclidean.oned.Interval;
25  import org.apache.commons.numbers.core.Precision;
26  import org.junit.jupiter.api.Assertions;
27  import org.junit.jupiter.api.Test;
28  
29  class RayTest {
30  
31      private static final double TEST_EPS = 1e-10;
32  
33      private static final Precision.DoubleEquivalence TEST_PRECISION =
34              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
35  
36      @Test
37      void testFromPointAndDirection() {
38          // arrange
39          final Vector2D p0 = Vector2D.of(1, 2);
40          final Vector2D p1 = Vector2D.of(2, 2);
41  
42          // act
43          final Ray ray = Lines.rayFromPointAndDirection(p0, p0.vectorTo(p1), TEST_PRECISION);
44  
45          // assert
46          Assertions.assertFalse(ray.isFull());
47          Assertions.assertFalse(ray.isEmpty());
48          Assertions.assertTrue(ray.isInfinite());
49          Assertions.assertFalse(ray.isFinite());
50  
51          EuclideanTestUtils.assertCoordinatesEqual(p0, ray.getStartPoint(), TEST_EPS);
52          Assertions.assertNull(ray.getEndPoint());
53  
54          Assertions.assertEquals(1, ray.getSubspaceStart(), TEST_EPS);
55          GeometryTestUtils.assertPositiveInfinity(ray.getSubspaceEnd());
56  
57          GeometryTestUtils.assertPositiveInfinity(ray.getSize());
58          Assertions.assertNull(ray.getCentroid());
59          Assertions.assertNull(ray.getBounds());
60  
61          EuclideanTestUtils.assertCoordinatesEqual(p0.vectorTo(p1), ray.getDirection(), TEST_EPS);
62      }
63  
64      @Test
65      void testFromPointAndDirection_invalidArgs() {
66          // arrange
67          final Vector2D p = Vector2D.of(0, 2);
68          final Vector2D d = Vector2D.of(1e-17, -1e-12);
69  
70          // act/assert
71          GeometryTestUtils.assertThrowsWithMessage(() -> {
72              Lines.rayFromPointAndDirection(p, d, TEST_PRECISION);
73          }, IllegalArgumentException.class, "Line direction cannot be zero");
74      }
75  
76      @Test
77      void testFromPoint() {
78          // arrange
79          final Vector2D p0 = Vector2D.of(1, 1);
80          final Vector2D p1 = Vector2D.of(1, 2);
81          final Vector2D p3 = Vector2D.of(3, 3);
82  
83          final Line line = Lines.fromPoints(p0, p1, TEST_PRECISION);
84  
85          // act
86          final Ray ray = Lines.rayFromPoint(line, p3);
87  
88          // assert
89          Assertions.assertFalse(ray.isFull());
90          Assertions.assertFalse(ray.isEmpty());
91          Assertions.assertTrue(ray.isInfinite());
92          Assertions.assertFalse(ray.isFinite());
93  
94          EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 3), ray.getStartPoint(), TEST_EPS);
95          Assertions.assertNull(ray.getEndPoint());
96  
97          Assertions.assertEquals(3, ray.getSubspaceStart(), TEST_EPS);
98          GeometryTestUtils.assertPositiveInfinity(ray.getSubspaceEnd());
99  
100         GeometryTestUtils.assertPositiveInfinity(ray.getSize());
101         Assertions.assertNull(ray.getCentroid());
102         Assertions.assertNull(ray.getBounds());
103 
104         EuclideanTestUtils.assertCoordinatesEqual(p0.vectorTo(p1), ray.getDirection(), TEST_EPS);
105     }
106 
107     @Test
108     void testFromPoint_invalidArgs() {
109         // arrange
110         final Vector2D p = Vector2D.of(0, 2);
111         final Vector2D d = Vector2D.of(1, 1);
112         final Line line = Lines.fromPointAndDirection(p, d, TEST_PRECISION);
113 
114         // act/assert
115         GeometryTestUtils.assertThrowsWithMessage(() -> {
116             Lines.rayFromPoint(line, Vector2D.NaN);
117         }, IllegalArgumentException.class, "Invalid ray start point: (NaN, NaN)");
118 
119         GeometryTestUtils.assertThrowsWithMessage(() -> {
120             Lines.rayFromPoint(line, Vector2D.POSITIVE_INFINITY);
121         }, IllegalArgumentException.class, "Invalid ray start point: (Infinity, Infinity)");
122 
123         GeometryTestUtils.assertThrowsWithMessage(() -> {
124             Lines.rayFromPoint(line, Vector2D.NEGATIVE_INFINITY);
125         }, IllegalArgumentException.class, "Invalid ray start point: (-Infinity, -Infinity)");
126     }
127 
128     @Test
129     void testFromLocation() {
130         // arrange
131         final Vector2D p0 = Vector2D.of(1, 1);
132         final Vector2D p1 = Vector2D.of(1, 2);
133 
134         final Line line = Lines.fromPoints(p0, p1, TEST_PRECISION);
135 
136         // act
137         final Ray ray = Lines.rayFromLocation(line, -2);
138 
139         // assert
140         Assertions.assertFalse(ray.isFull());
141         Assertions.assertFalse(ray.isEmpty());
142         Assertions.assertTrue(ray.isInfinite());
143         Assertions.assertFalse(ray.isFinite());
144 
145         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, -2), ray.getStartPoint(), TEST_EPS);
146         Assertions.assertNull(ray.getEndPoint());
147 
148         Assertions.assertEquals(-2, ray.getSubspaceStart(), TEST_EPS);
149         GeometryTestUtils.assertPositiveInfinity(ray.getSubspaceEnd());
150 
151         GeometryTestUtils.assertPositiveInfinity(ray.getSize());
152         Assertions.assertNull(ray.getCentroid());
153         Assertions.assertNull(ray.getBounds());
154 
155         EuclideanTestUtils.assertCoordinatesEqual(p0.vectorTo(p1), ray.getDirection(), TEST_EPS);
156     }
157 
158     @Test
159     void testFromLocation_invalidArgs() {
160         // arrange
161         final Vector2D p = Vector2D.of(0, 2);
162         final Vector2D d = Vector2D.of(1, 1);
163         final Line line = Lines.fromPointAndDirection(p, d, TEST_PRECISION);
164 
165         // act/assert
166         GeometryTestUtils.assertThrowsWithMessage(() -> {
167             Lines.rayFromLocation(line, Double.NaN);
168         }, IllegalArgumentException.class, "Invalid ray start location: NaN");
169 
170         GeometryTestUtils.assertThrowsWithMessage(() -> {
171             Lines.rayFromLocation(line, Double.POSITIVE_INFINITY);
172         }, IllegalArgumentException.class, "Invalid ray start location: Infinity");
173 
174         GeometryTestUtils.assertThrowsWithMessage(() -> {
175             Lines.rayFromLocation(line, Double.NEGATIVE_INFINITY);
176         }, IllegalArgumentException.class, "Invalid ray start location: -Infinity");
177     }
178 
179     @Test
180     void testTransform() {
181         // arrange
182         final AffineTransformMatrix2D t = AffineTransformMatrix2D.createRotation(-0.5 * Math.PI)
183                 .translate(Vector2D.Unit.PLUS_X);
184 
185         final Ray ray = Lines.rayFromPointAndDirection(Vector2D.of(1, 0), Vector2D.Unit.PLUS_X, TEST_PRECISION);
186 
187         // act
188         final Ray result = ray.transform(t);
189 
190         // assert
191         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, -1), result.getStartPoint(), TEST_EPS);
192         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_Y, result.getDirection(), TEST_EPS);
193     }
194 
195     @Test
196     void testTransform_reflection() {
197         // arrange
198         final AffineTransformMatrix2D t = AffineTransformMatrix2D.createRotation(0.5 * Math.PI)
199                 .translate(Vector2D.Unit.PLUS_X)
200                 .scale(1, -1);
201 
202         final Ray ray = Lines.rayFromPointAndDirection(Vector2D.of(2, 3), Vector2D.Unit.PLUS_X, TEST_PRECISION);
203 
204         // act
205         final Ray result = ray.transform(t);
206 
207         // assert
208         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2, -2), result.getStartPoint(), TEST_EPS);
209         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.Unit.MINUS_Y, result.getDirection(), TEST_EPS);
210     }
211 
212     @Test
213     void testReverse() {
214         // arrange
215         final Vector2D start = Vector2D.of(1, 2);
216 
217         EuclideanTestUtils.permuteSkipZero(-4, 4, 1, (x, y) -> {
218             final Vector2D dir = Vector2D.of(x, y);
219 
220             final Ray ray = Lines.rayFromPointAndDirection(start, dir, TEST_PRECISION);
221 
222             // act
223             final ReverseRay rev = ray.reverse();
224 
225             // assert
226             EuclideanTestUtils.assertCoordinatesEqual(ray.getLine().getOrigin(), rev.getLine().getOrigin(), TEST_EPS);
227             Assertions.assertEquals(-1, ray.getLine().getDirection().dot(rev.getLine().getDirection()), TEST_EPS);
228 
229             EuclideanTestUtils.assertCoordinatesEqual(ray.getStartPoint(), rev.getEndPoint(), TEST_EPS);
230         });
231     }
232 
233     @Test
234     void testClosest() {
235         // arrange
236         final Vector2D p1 = Vector2D.of(0, -1);
237         final Vector2D p2 = Vector2D.of(0, 1);
238         final Ray ray = Lines.rayFromPointAndDirection(p1, p1.directionTo(p2), TEST_PRECISION);
239 
240         // act/assert
241         EuclideanTestUtils.assertCoordinatesEqual(p1, ray.closest(p1), TEST_EPS);
242         EuclideanTestUtils.assertCoordinatesEqual(p1, ray.closest(Vector2D.of(0, -2)), TEST_EPS);
243         EuclideanTestUtils.assertCoordinatesEqual(p1, ray.closest(Vector2D.of(2, -2)), TEST_EPS);
244         EuclideanTestUtils.assertCoordinatesEqual(p1, ray.closest(Vector2D.of(-1, -1)), TEST_EPS);
245 
246         EuclideanTestUtils.assertCoordinatesEqual(p2, ray.closest(p2), TEST_EPS);
247         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 2), ray.closest(Vector2D.of(0, 2)), TEST_EPS);
248         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 2), ray.closest(Vector2D.of(-2, 2)), TEST_EPS);
249         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 1), ray.closest(Vector2D.of(-1, 1)), TEST_EPS);
250 
251         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, ray.closest(Vector2D.ZERO), TEST_EPS);
252         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 0.5), ray.closest(Vector2D.of(1, 0.5)), TEST_EPS);
253         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, -0.5), ray.closest(Vector2D.of(-2, -0.5)), TEST_EPS);
254     }
255 
256     @Test
257     void testClassify() {
258         // arrange
259         final Ray ray = Lines.rayFromPointAndDirection(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, TEST_PRECISION);
260 
261         // act/assert
262         EuclideanTestUtils.assertRegionLocation(ray, RegionLocation.OUTSIDE,
263                 Vector2D.of(2, 2), Vector2D.of(2, 0),
264                 Vector2D.of(-5, 1), Vector2D.of(0, 1));
265 
266         EuclideanTestUtils.assertRegionLocation(ray, RegionLocation.BOUNDARY,
267                 Vector2D.of(1, 1), Vector2D.of(1 + 1e-16, 1));
268 
269         EuclideanTestUtils.assertRegionLocation(ray, RegionLocation.INSIDE,
270                 Vector2D.of(2, 1), Vector2D.of(5, 1 + 1e-16));
271     }
272 
273     @Test
274     void testSplit() {
275         // --- arrange
276         final Vector2D p0 = Vector2D.of(1, 1);
277         final Vector2D p1 = Vector2D.of(3, 1);
278         final Vector2D low = Vector2D.of(0, 1);
279 
280         final Vector2D delta = Vector2D.of(1e-11, 1e-11);
281 
282         final Ray ray = Lines.rayFromPointAndDirection(p0, Vector2D.Unit.PLUS_X, TEST_PRECISION);
283 
284         // --- act
285 
286         // parallel
287         checkSplit(ray.split(Lines.fromPointAndAngle(Vector2D.of(2, 2), 0, TEST_PRECISION)),
288                 null, null,
289                 p0, null);
290         checkSplit(ray.split(Lines.fromPointAndAngle(Vector2D.of(2, 2), Math.PI, TEST_PRECISION)),
291                 p0, null,
292                 null, null);
293 
294         // coincident
295         checkSplit(ray.split(Lines.fromPointAndAngle(p0.add(delta), 1e-20, TEST_PRECISION)),
296                 null, null,
297                 null, null);
298 
299         // through point on ray
300         checkSplit(ray.split(Lines.fromPointAndAngle(p1, 1, TEST_PRECISION)),
301                 p0, p1,
302                 p1, null);
303         checkSplit(ray.split(Lines.fromPointAndAngle(p1, -1, TEST_PRECISION)),
304                 p1, null,
305                 p0, p1);
306 
307         // through start point
308         checkSplit(ray.split(Lines.fromPointAndAngle(p0.subtract(delta), 1, TEST_PRECISION)),
309                 null, null,
310                 p0, null);
311         checkSplit(ray.split(Lines.fromPointAndAngle(p0.add(delta), -1, TEST_PRECISION)),
312                 p0, null,
313                 null, null);
314 
315         // intersection below minus
316         checkSplit(ray.split(Lines.fromPointAndAngle(low, 1, TEST_PRECISION)),
317                 null, null,
318                 p0, null);
319         checkSplit(ray.split(Lines.fromPointAndAngle(low, -1, TEST_PRECISION)),
320                 p0, null,
321                 null, null);
322     }
323 
324     @Test
325     void testSplit_smallAngle_pointOnSplitter() {
326         // arrange
327         final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-5);
328 
329         final Ray ray = Lines.rayFromPointAndDirection(Vector2D.of(1, 1e-6), Vector2D.of(-1, -1e-2), precision);
330 
331         final Line splitter = Lines.fromPointAndAngle(Vector2D.ZERO, 0, precision);
332 
333         // act
334         final Split<LineConvexSubset> split = ray.split(splitter);
335 
336         // assert
337         Assertions.assertEquals(SplitLocation.PLUS, split.getLocation());
338 
339         Assertions.assertNull(split.getMinus());
340         Assertions.assertSame(ray, split.getPlus());
341     }
342 
343     @Test
344     void testGetInterval() {
345         // arrange
346         final Ray ray = Lines.rayFromPointAndDirection(Vector2D.of(2, -1), Vector2D.Unit.PLUS_X, TEST_PRECISION);
347 
348         // act
349         final Interval interval = ray.getInterval();
350 
351         // assert
352         Assertions.assertEquals(2, interval.getMin(), TEST_EPS);
353         GeometryTestUtils.assertPositiveInfinity(interval.getMax());
354 
355         Assertions.assertSame(ray.getLine().getPrecision(), interval.getMinBoundary().getPrecision());
356     }
357 
358     @Test
359     void testToString() {
360         // arrange
361         final Ray ray = Lines.rayFromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION);
362 
363         // act
364         final String str = ray.toString();
365 
366         // assert
367         GeometryTestUtils.assertContains("Ray[startPoint= (0", str);
368         GeometryTestUtils.assertContains(", direction= (1", str);
369     }
370 
371     private static void checkSplit(final Split<LineConvexSubset> split, final Vector2D minusStart, final Vector2D minusEnd,
372                                    final Vector2D plusStart, final Vector2D plusEnd) {
373 
374         final LineConvexSubset minus = split.getMinus();
375         if (minusStart == null && minusEnd == null) {
376             Assertions.assertNull(minus);
377         } else {
378             checkPoint(minusStart, minus.getStartPoint());
379             checkPoint(minusEnd, minus.getEndPoint());
380         }
381 
382 
383         final LineConvexSubset plus = split.getPlus();
384         if (plusStart == null && plusEnd == null) {
385             Assertions.assertNull(plus);
386         } else {
387             checkPoint(plusStart, plus.getStartPoint());
388             checkPoint(plusEnd, plus.getEndPoint());
389         }
390     }
391 
392     private static void checkPoint(final Vector2D expected, final Vector2D pt) {
393         if (expected == null) {
394             Assertions.assertNull(pt);
395         } else {
396             EuclideanTestUtils.assertCoordinatesEqual(expected, pt, TEST_EPS);
397         }
398     }
399 }