This is an automated email from the ASF dual-hosted git repository. erans pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-geometry.git
commit 128f322ff5a329f80692cedcaf4f5e37cd2129ae Author: Matt Juntunen <matt.juntu...@hotmail.com> AuthorDate: Mon Sep 17 22:49:58 2018 -0400 GEOMETRY-17: adding Vector2D#orthogonal methods --- .../geometry/euclidean/threed/Vector3D.java | 2 +- .../commons/geometry/euclidean/twod/Vector2D.java | 47 +++++++++-- .../geometry/euclidean/twod/Vector2DTest.java | 95 ++++++++++++++++++++++ 3 files changed, 136 insertions(+), 8 deletions(-) diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java index 9e81e61..9f9bb03 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java @@ -198,7 +198,7 @@ public class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Ve * <pre><code> * Vector3D k = u.normalize(); * Vector3D i = k.orthogonal(); - * Vector3D j = Vector3D.crossProduct(k, i); + * Vector3D j = k.crossProduct(i); * </code></pre> * @return a new normalized vector orthogonal to the instance * @exception IllegalNormException if the norm of the instance is zero, NaN, diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java index 6f78508..dc3b925 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java @@ -17,6 +17,7 @@ package org.apache.commons.geometry.euclidean.twod; import org.apache.commons.geometry.core.exception.IllegalNormException; +import org.apache.commons.geometry.core.internal.DoubleFunction2N; import org.apache.commons.geometry.core.internal.SimpleTupleFormat; import org.apache.commons.geometry.euclidean.EuclideanVector; import org.apache.commons.geometry.euclidean.internal.Vectors; @@ -200,13 +201,38 @@ public class Vector2D extends Cartesian2D implements EuclideanVector<Point2D, Ve /** {@inheritDoc} */ @Override public Vector2D project(Vector2D base) { - return getComponent(base, false); + return getComponent(base, false, Vector2D::new); } /** {@inheritDoc} */ @Override public Vector2D reject(Vector2D base) { - return getComponent(base, true); + return getComponent(base, true, Vector2D::new); + } + + /** Returns a unit vector orthogonal to the current vector by rotating the + * vector {@code pi/2} radians counterclockwise around the origin. For example, + * if this method is called on the vector representing the positive x-axis, then + * a vector representing the positive y-axis is returned. + * @return a unit vector orthogonal to the current instance + * @throws IllegalNormException if the norm of the current instance is zero, NaN, + * or infinite + */ + public Vector2D orthogonal() { + return normalize(-getY(), getX()); + } + + /** Returns a unit vector orthogonal to the current vector and pointing in the direction + * of {@code dir}. This method is equivalent to calling {@code dir.reject(vec).normalize()} + * except that no intermediate vector object is produced. + * @param dir the direction to use for generating the orthogonal vector + * @return unit vector orthogonal to the current vector and pointing in the direction of + * {@code dir} that does not lie along the current vector + * @throws IllegalNormException if either vector norm is zero, NaN or infinite, + * or the given vector is collinear with this vector. + */ + public Vector2D orthogonal(Vector2D dir) { + return dir.getComponent(this, true, Vector2D::normalize); } /** {@inheritDoc} @@ -321,25 +347,32 @@ public class Vector2D extends Cartesian2D implements EuclideanVector<Point2D, Ve * @param reject If true, the rejection of this instance from {@code base} is * returned. If false, the projection of this instance onto {@code base} * is returned. + * @param factory factory function used to build the final vector * @return The projection or rejection of this instance relative to {@code base}, * depending on the value of {@code reject}. * @throws IllegalNormException if {@code base} has a zero, NaN, or infinite norm */ - private Vector2D getComponent(Vector2D base, boolean reject) { + private Vector2D getComponent(Vector2D base, boolean reject, DoubleFunction2N<Vector2D> factory) { final double aDotB = dotProduct(base); - final double baseMag = Vectors.ensureRealNonZeroNorm(base.getNorm()); + // We need to check the norm value here to ensure that it's legal. However, we don't + // want to incur the cost or floating point error of getting the actual norm and then + // multiplying it again to get the square norm. So, we'll just check the squared norm + // directly. This will produce the same error result as checking the actual norm since + // Math.sqrt(0.0) == 0.0, Math.sqrt(Double.NaN) == Double.NaN and + // Math.sqrt(Double.POSITIVE_INFINITY) == Double.POSITIVE_INFINITY. + final double baseMagSq = Vectors.ensureRealNonZeroNorm(base.getNormSq()); - final double scale = aDotB / (baseMag * baseMag); + final double scale = aDotB / baseMagSq; final double projX = scale * base.getX(); final double projY = scale * base.getY(); if (reject) { - return new Vector2D(getX() - projX, getY() - projY); + return factory.apply(getX() - projX, getY() - projY); } - return new Vector2D(projX, projY); + return factory.apply(projX, projY); } /** Returns a vector with the given coordinate values. diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java index 9d61862..19b4814 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java @@ -405,6 +405,101 @@ public class Vector2DTest { } @Test + public void testOrthogonal() { + // arrange + double invSqrt2 = 1.0 / Math.sqrt(2.0); + + // act/assert + checkVector(Vector2D.PLUS_X.orthogonal(), 0.0, 1.0); + checkVector(Vector2D.of(1.0, 1.0).orthogonal(), -invSqrt2, invSqrt2); + + checkVector(Vector2D.PLUS_Y.orthogonal(), -1.0, 0.0); + checkVector(Vector2D.of(-1.0, 1.0).orthogonal(), -invSqrt2, -invSqrt2); + + checkVector(Vector2D.MINUS_X.orthogonal(), 0.0, -1.0); + checkVector(Vector2D.of(-1.0, -1.0).orthogonal(), invSqrt2, -invSqrt2); + + checkVector(Vector2D.MINUS_Y.orthogonal(), 1.0, 0.0); + checkVector(Vector2D.of(1.0, -1.0).orthogonal(), invSqrt2, invSqrt2); + } + + @Test + public void testOrthogonal_fullCircle() { + for (double az = 0.0; az<=Geometry.TWO_PI; az += 0.25) { + // arrange + Vector2D v = Vector2D.ofPolar(Math.PI, az); + + //act + Vector2D ortho = v.orthogonal(); + + // assert + Assert.assertEquals(1.0, ortho.getNorm(), EPS); + Assert.assertEquals(0.0, v.dotProduct(ortho), EPS); + } + } + + @Test + public void testOrthogonal_illegalNorm() { + // act/assert + GeometryTestUtils.assertThrows(() -> Vector2D.ZERO.orthogonal(), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.NaN.orthogonal(), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.POSITIVE_INFINITY.orthogonal(), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.NEGATIVE_INFINITY.orthogonal(), + IllegalNormException.class); + } + + @Test + public void testOrthogonal_givenDirection() { + // arrange + double invSqrt2 = 1.0 / Math.sqrt(2.0); + + // act/assert + checkVector(Vector2D.PLUS_X.orthogonal(Vector2D.of(-1.0, 0.1)), 0.0, 1.0); + checkVector(Vector2D.PLUS_Y.orthogonal(Vector2D.of(2.0, 2.0)), 1.0, 0.0); + + checkVector(Vector2D.of(2.9, 2.9).orthogonal(Vector2D.of(1.0, 0.22)), invSqrt2, -invSqrt2); + checkVector(Vector2D.of(2.9, 2.9).orthogonal(Vector2D.of(0.22, 1.0)), -invSqrt2, invSqrt2); + } + + @Test + public void testOrthogonal_givenDirection_illegalNorm() { + // act/assert + GeometryTestUtils.assertThrows(() -> Vector2D.ZERO.orthogonal(Vector2D.PLUS_X), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.NaN.orthogonal(Vector2D.PLUS_X), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.POSITIVE_INFINITY.orthogonal(Vector2D.PLUS_X), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.NEGATIVE_INFINITY.orthogonal(Vector2D.PLUS_X), + IllegalNormException.class); + + GeometryTestUtils.assertThrows(() -> Vector2D.PLUS_X.orthogonal(Vector2D.ZERO), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.PLUS_X.orthogonal(Vector2D.NaN), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.PLUS_X.orthogonal(Vector2D.POSITIVE_INFINITY), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.PLUS_X.orthogonal(Vector2D.NEGATIVE_INFINITY), + IllegalNormException.class); + } + + @Test + public void testOrthogonal_givenDirection_directionIsCollinear() { + // act/assert + GeometryTestUtils.assertThrows(() -> Vector2D.PLUS_X.orthogonal(Vector2D.PLUS_X), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.PLUS_X.orthogonal(Vector2D.MINUS_X), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.of(1.0, 1.0).orthogonal(Vector2D.of(2.0, 2.0)), + IllegalNormException.class); + GeometryTestUtils.assertThrows(() -> Vector2D.of(-1.01, -1.01).orthogonal(Vector2D.of(20.1, 20.1)), + IllegalNormException.class); + } + + @Test public void testAngle() { // act/assert Assert.assertEquals(0, Vector2D.PLUS_X.angle(Vector2D.PLUS_X), EPS);