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 76086cc615ce129629b2f5a4a91baa1c3c3b3467 Author: Matt Juntunen <[email protected]> AuthorDate: Thu Jun 21 23:39:15 2018 -0400 GEOMETRY-7: adding PolarCoordinates class --- .../org/apache/commons/geometry/core/Geometry.java | 15 +- .../apache/commons/geometry/core/GeometryTest.java | 64 ++++ .../geometry/euclidean/twod/Cartesian2D.java | 7 + .../commons/geometry/euclidean/twod/Point2D.java | 9 + .../geometry/euclidean/twod/PolarCoordinates.java | 300 +++++++++++++++++ .../commons/geometry/euclidean/twod/Vector2D.java | 9 + .../geometry/euclidean/twod/Cartesian2DTest.java | 33 ++ .../geometry/euclidean/twod/Point2DTest.java | 31 +- .../euclidean/twod/PolarCoordinatesTest.java | 360 +++++++++++++++++++++ .../geometry/euclidean/twod/Vector2DTest.java | 30 +- 10 files changed, 848 insertions(+), 10 deletions(-) diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java index d74d312..efe797d 100644 --- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java +++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java @@ -23,16 +23,19 @@ public class Geometry { /** Alias for {@link Math#PI}, placed here for completeness. */ public static final double PI = Math.PI; - /** Constant value for {@code 2*pi}. - */ + /** Constant value for {@code -pi} */ + public static final double MINUS_PI = - Math.PI; + + /** Constant value for {@code 2*pi}. */ public static final double TWO_PI = 2.0 * Math.PI; - /** Constant value for {@code pi / 2}. - */ + /** Constant value for {@code -2*pi}. */ + public static final double MINUS_TWO_PI = -2.0 * Math.PI; + + /** Constant value for {@code pi / 2}. */ public static final double HALF_PI = 0.5 * Math.PI; - /** Constant value for {@code - pi / 2}. - */ + /** Constant value for {@code - pi / 2}. */ public static final double MINUS_HALF_PI = - 0.5 * Math.PI; /** Private constructor */ diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTest.java new file mode 100644 index 0000000..88533eb --- /dev/null +++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.geometry.core; + +import org.junit.Assert; +import org.junit.Test; + +public class GeometryTest { + + @Test + public void testConstants() { + // arrange + double eps = 0.0; + + // act/assert + Assert.assertEquals(Math.PI, Geometry.PI, eps); + Assert.assertEquals(-Math.PI, Geometry.MINUS_PI, eps); + + Assert.assertEquals(2.0 * Math.PI, Geometry.TWO_PI, eps); + Assert.assertEquals(-2.0 * Math.PI, Geometry.MINUS_TWO_PI, eps); + + Assert.assertEquals(Math.PI / 2.0, Geometry.HALF_PI, 0.0); + Assert.assertEquals(-Math.PI / 2.0, Geometry.MINUS_HALF_PI, eps); + } + + @Test + public void testConstants_trigEval() { + // arrange + double eps = 1e-15; + + // act/assert + Assert.assertEquals(0.0, Math.sin(Geometry.PI), eps); + Assert.assertEquals(-1.0, Math.cos(Geometry.PI), eps); + + Assert.assertEquals(0.0, Math.sin(Geometry.MINUS_PI), eps); + Assert.assertEquals(-1.0, Math.cos(Geometry.MINUS_PI), eps); + + Assert.assertEquals(0.0, Math.sin(Geometry.TWO_PI), eps); + Assert.assertEquals(1.0, Math.cos(Geometry.TWO_PI), eps); + + Assert.assertEquals(0.0, Math.sin(Geometry.MINUS_TWO_PI), eps); + Assert.assertEquals(1.0, Math.cos(Geometry.MINUS_TWO_PI), eps); + + Assert.assertEquals(1.0, Math.sin(Geometry.HALF_PI), eps); + Assert.assertEquals(0.0, Math.cos(Geometry.HALF_PI), eps); + + Assert.assertEquals(-1.0, Math.sin(Geometry.MINUS_HALF_PI), eps); + Assert.assertEquals(0.0, Math.cos(Geometry.MINUS_HALF_PI), eps); + } +} diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Cartesian2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Cartesian2D.java index d4c69b6..8710b3e 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Cartesian2D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Cartesian2D.java @@ -59,6 +59,13 @@ public abstract class Cartesian2D implements Spatial, Serializable { return y; } + /** Return an equivalent set of coordinates in polar form. + * @return An equivalent set of coordinates in polar form. + */ + public PolarCoordinates toPolar() { + return PolarCoordinates.ofCartesian(x, y); + } + /** Get the coordinates for this instance as a dimension 2 array. * @return coordinates for this instance */ diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Point2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Point2D.java index c32777a..6561570 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Point2D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Point2D.java @@ -178,6 +178,15 @@ public final class Point2D extends Cartesian2D implements EuclideanPoint<Point2D return new Point2D(p[0], p[1]); } + /**Return a point with coordinates equivalent to the given set of polar coordinates. + * @param radius The polar coordinate radius value. + * @param azimuth The polar coordinate azimuth angle in radians. + * @return point instance with coordinates equivalent to the given polar coordinates. + */ + public static Point2D ofPolar(final double radius, final double azimuth) { + return PolarCoordinates.toCartesian(radius, azimuth, getFactory()); + } + /** Parses the given string and returns a new point instance. The expected string * format is the same as that returned by {@link #toString()}. * @param str the string to parse diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java new file mode 100644 index 0000000..de3731c --- /dev/null +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.geometry.euclidean.twod; + +import java.io.Serializable; +import java.text.ParsePosition; + +import org.apache.commons.geometry.core.Geometry; +import org.apache.commons.geometry.core.Spatial; +import org.apache.commons.geometry.core.util.AbstractCoordinateParser; +import org.apache.commons.geometry.core.util.Coordinates; +import org.apache.commons.numbers.angle.PlaneAngleRadians; + +/** Class representing a set of polar coordinates in 2 dimensional + * Euclidean space. Coordinates are normalized so that {@code radius >= 0} + * and {@code -pi < azimuth <= pi}. + */ +public class PolarCoordinates implements Spatial, Serializable { + + /** Serializable version UID */ + private static final long serialVersionUID = -3122872387910228544L; + + /** Shared parser/formatter instance **/ + private static final PolarCoordinatesParser PARSER = new PolarCoordinatesParser(); + + /** Radius value */ + private final double radius; + + /** Azimuth angle in radians. */ + private final double azimuth; + + /** Simple constructor. Input values must already be normalized. + * @param radius Radius value. + * @param azimuth Azimuth angle in radians. + */ + private PolarCoordinates(final double radius, final double azimuth) { + this.radius = radius; + this.azimuth = azimuth; + } + + /** Return the radius value. The value will be greater than or equal to 0. + * @return radius value + */ + public double getRadius() { + return radius; + } + + /** Return the azimuth angle in radians. The value will be + * in the range {@code (-pi, pi]}. + * @return azimuth value in radians. + */ + public double getAzimuth() { + return azimuth; + } + + /** {@inheritDoc} */ + @Override + public int getDimension() { + return 2; + } + + /** {@inheritDoc} */ + @Override + public boolean isNaN() { + return Double.isNaN(radius) || Double.isNaN(azimuth); + } + + /** {@inheritDoc} */ + @Override + public boolean isInfinite() { + return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth)); + } + + /** Convert this set of polar coordinates to Cartesian coordinates. + * The Cartesian coordinates are computed and passed to the given + * factory instance. The factory's return value is returned. + * @param factory Factory instance that will be passed the computed Cartesian coordinates + * @return the value returned by the given factory when passed Cartesian + * coordinates equivalent to this set of polar coordinates. + */ + public <T> T toCartesian(final Coordinates.Factory2D<T> factory) { + return toCartesian(radius, azimuth, factory); + } + + /** Convert this set of polar coordinates to a 2-dimensional + * vector. + * @return A 2-dimensional vector with an equivalent set of + * coordinates. + */ + public Vector2D toVector() { + return toCartesian(Vector2D.getFactory()); + } + + /** Convert this set of polar coordinates to a 2-dimensional + * point. + * @return A 2-dimensional point with an equivalent set of + * coordinates. + */ + public Point2D toPoint() { + return toCartesian(Point2D.getFactory()); + } + + /** + * Get a hashCode for this set of polar coordinates. + * <p>All NaN values have the same hash code.</p> + * + * @return a hash code value for this object + */ + @Override + public int hashCode() { + if (isNaN()) { + return 191; + } + return 449 * (76 * Double.hashCode(radius) + Double.hashCode(azimuth)); + } + + /** Test for the equality of two sets of polar coordinates. + * <p> + * If all values of two sets of coordinates are exactly the same, and none are + * <code>Double.NaN</code>, the two sets are considered to be equal. + * </p> + * <p> + * <code>NaN</code> values are considered to globally affect the coordinates + * and be equal to each other - i.e, if either (or all) values of the + * coordinate set are equal to <code>Double.NaN</code>, the set is equal to + * {@link #NaN}. + * </p> + * + * @param other Object to test for equality to this + * @return true if two PolarCoordinates objects are equal, false if + * object is null, not an instance of PolarCoordinates, or + * not equal to this PolarCoordinates instance + * + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other instanceof PolarCoordinates) { + final PolarCoordinates rhs = (PolarCoordinates) other; + if (rhs.isNaN()) { + return this.isNaN(); + } + + return (radius == rhs.radius) && (azimuth == rhs.azimuth); + } + return false; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return PARSER.format(this); + } + + /** Return a new polar coordinate instance with the given values. + * The values are normalized so that radius lies in the range {@code [0, +infinity)} + * and azimuth in the range {@code (-pi, pi]}. + * @param radius Radius value. + * @param azimuth Azimuth angle in radians. + * @return + */ + public static PolarCoordinates of(double radius, double azimuth) { + if (radius < 0) { + radius = Math.abs(radius); + azimuth += Geometry.PI; + } + + if (Double.isFinite(azimuth)) { + azimuth = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(azimuth); + + // the above normalizes the azimuth to -pi <= azimuth <= pi + // but we want -pi < azimuth <= pi to have completely unique coordinates + if (azimuth <= -Geometry.PI) { + azimuth += Geometry.TWO_PI; + } + } + + return new PolarCoordinates(radius, azimuth); + } + + /** Convert the given Cartesian coordinates to polar form. + * @param x X coordinate value + * @param y Y coordinate value + * @return polar coordinates equivalent to the given Cartesian coordinates + */ + public static PolarCoordinates ofCartesian(final double x, final double y) { + final double azimuth = Math.atan2(y, x); + final double radius = Math.hypot(x, y); + + return new PolarCoordinates(radius, azimuth); + } + + /** Parse the given string and return a new polar coordinates instance. The parsed + * coordinates are normalized so that radius is within the range {@code [0, +infinity)} + * and azimuth is within the range {@code (-pi, pi]}. The expected string + * format is the same as that returned by {@link #toString()}. + * @param input the string to parse + * @return + */ + public static PolarCoordinates parse(String input) { + return PARSER.parse(input); + } + + /** Convert the given set of polar coordinates to Cartesian coordinates. + * The Cartesian coordinates are computed and passed to the given + * factory instance. The factory's return value is returned. + * @param radius Radius value + * @param azimuth Azimuth value in radians + * @param factory Factory instance that will be passed the computed Cartesian coordinates + * @param <T> Type returned by the factory + * @return the value returned by the given factory when passed Cartesian + * coordinates equivalent to given set of polar coordinates. + */ + public static <T> T toCartesian(final double radius, final double azimuth, final Coordinates.Factory2D<T> factory) { + final double x = radius * Math.cos(azimuth); + final double y = radius * Math.sin(azimuth); + + return factory.create(x, y); + } + + /** Parser and formatter class for polar coordinates. */ + private static class PolarCoordinatesParser extends AbstractCoordinateParser { + + /** String prefix for the radius value. */ + private static final String RADIUS_PREFIX = "r="; + + /** String prefix for the azimuth value. */ + private static final String AZIMUTH_PREFIX = "az="; + + /** Simple constructor. */ + private PolarCoordinatesParser() { + super(",", "(", ")"); + } + + /** Return a standardized string representation of the given set of polar + * coordinates. + * @param polar coordinates to format + * @return a standard string representation of the polar coordinates + */ + public String format(PolarCoordinates polar) { + final StringBuilder sb = new StringBuilder(); + + sb.append(getPrefix()); + + sb.append(RADIUS_PREFIX); + sb.append(polar.getRadius()); + + sb.append(getSeparator()); + sb.append(" "); + + sb.append(AZIMUTH_PREFIX); + sb.append(polar.getAzimuth()); + + sb.append(getSuffix()); + + return sb.toString(); + } + + /** Parse the given string and return a set of standardized polar coordinates. + * @param str the string to parse + * @return polar coordinates + */ + public PolarCoordinates parse(String str) { + final ParsePosition pos = new ParsePosition(0); + + readPrefix(str, pos); + + consumeWhitespace(str, pos); + readSequence(str, RADIUS_PREFIX, pos); + final double radius = readCoordinateValue(str, pos); + + consumeWhitespace(str, pos); + readSequence(str, AZIMUTH_PREFIX, pos); + final double azimuth = readCoordinateValue(str, pos); + + readSuffix(str, pos); + endParse(str, pos); + + // use the factory method so that the values will be normalized + return PolarCoordinates.of(radius, azimuth); + } + } +} 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 b6c67af..e2e2e56 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 @@ -377,6 +377,15 @@ public final class Vector2D extends Cartesian2D implements EuclideanVector<Point return new Vector2D(v[0], v[1]); } + /** Return a vector with coordinates equivalent to the given set of polar coordinates. + * @param radius The polar coordinate radius value. + * @param azimuth The polar coordinate azimuth angle in radians. + * @return vector instance with coordinates equivalent to the given polar coordinates. + */ + public static Vector2D ofPolar(final double radius, final double azimuth) { + return PolarCoordinates.toCartesian(radius, azimuth, getFactory()); + } + /** Parses the given string and returns a new vector instance. The expected string * format is the same as that returned by {@link #toString()}. * @param str the string to parse diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Cartesian2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Cartesian2DTest.java index b8c7238..379269f 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Cartesian2DTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Cartesian2DTest.java @@ -1,5 +1,6 @@ package org.apache.commons.geometry.euclidean.twod; +import org.apache.commons.geometry.core.Geometry; import org.junit.Assert; import org.junit.Test; @@ -68,6 +69,38 @@ public class Cartesian2DTest { Assert.assertFalse(new StubCartesian2D(Double.NaN, Double.POSITIVE_INFINITY).isInfinite()); } + @Test + public void testToPolar() { + // arrange + double sqrt2 = Math.sqrt(2.0); + + // act/assert + checkPolar(new StubCartesian2D(0, 0).toPolar(), 0, 0); + + checkPolar(new StubCartesian2D(1, 0).toPolar(), 1, 0); + checkPolar(new StubCartesian2D(-1, 0).toPolar(), 1, Geometry.PI); + + checkPolar(new StubCartesian2D(0, 2).toPolar(), 2, Geometry.HALF_PI); + checkPolar(new StubCartesian2D(0, -2).toPolar(), 2, Geometry.MINUS_HALF_PI); + + checkPolar(new StubCartesian2D(sqrt2, sqrt2).toPolar(), 2, 0.25 * Geometry.PI); + checkPolar(new StubCartesian2D(-sqrt2, sqrt2).toPolar(), 2, 0.75 * Geometry.PI); + checkPolar(new StubCartesian2D(sqrt2, -sqrt2).toPolar(), 2, -0.25 * Geometry.PI); + checkPolar(new StubCartesian2D(-sqrt2, -sqrt2).toPolar(), 2, -0.75 * Geometry.PI); + } + + @Test + public void testToPolar_NaNAndInfinite() { + // act/assert + Assert.assertTrue(new StubCartesian2D(Double.NaN, Double.NaN).toPolar().isNaN()); + Assert.assertTrue(new StubCartesian2D(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).toPolar().isInfinite()); + } + + private void checkPolar(PolarCoordinates polar, double radius, double azimuth) { + Assert.assertEquals(radius, polar.getRadius(), TEST_TOLERANCE); + Assert.assertEquals(azimuth, polar.getAzimuth(), TEST_TOLERANCE); + } + private static class StubCartesian2D extends Cartesian2D { private static final long serialVersionUID = 1L; diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Point2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Point2DTest.java index 20d95ad..f71ce00 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Point2DTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Point2DTest.java @@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.twod; import java.util.regex.Pattern; +import org.apache.commons.geometry.core.Geometry; import org.apache.commons.geometry.core.util.Coordinates; import org.apache.commons.numbers.core.Precision; import org.junit.Assert; @@ -220,6 +221,28 @@ public class Point2DTest { } @Test + public void testOfPolar() { + // arrange + double eps = 1e-15; + double sqrt2 = Math.sqrt(2.0); + + // act/assert + checkPoint(Point2D.ofPolar(0, 0), 0, 0, eps); + checkPoint(Point2D.ofPolar(1, 0), 1, 0, eps); + + checkPoint(Point2D.ofPolar(2, Geometry.PI), -2, 0, eps); + checkPoint(Point2D.ofPolar(-2, Geometry.PI), 2, 0, eps); + + checkPoint(Point2D.ofPolar(2, Geometry.HALF_PI), 0, 2, eps); + checkPoint(Point2D.ofPolar(-2, Geometry.HALF_PI), 0, -2, eps); + + checkPoint(Point2D.ofPolar(2, 0.25 * Geometry.PI), sqrt2, sqrt2, eps); + checkPoint(Point2D.ofPolar(2, 0.75 * Geometry.PI), -sqrt2, sqrt2, eps); + checkPoint(Point2D.ofPolar(2, -0.25 * Geometry.PI), sqrt2, - sqrt2, eps); + checkPoint(Point2D.ofPolar(2, -0.75 * Geometry.PI), -sqrt2, - sqrt2, eps); + } + + @Test public void testGetFactory() { // act Coordinates.Factory2D<Point2D> factory = Point2D.getFactory(); @@ -286,7 +309,11 @@ public class Point2DTest { } private void checkPoint(Point2D p, double x, double y) { - Assert.assertEquals(x, p.getX(), EPS); - Assert.assertEquals(y, p.getY(), EPS); + checkPoint(p, x, y, EPS); + } + + private void checkPoint(Point2D p, double x, double y, double eps) { + Assert.assertEquals(x, p.getX(), eps); + Assert.assertEquals(y, p.getY(), eps); } } diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinatesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinatesTest.java new file mode 100644 index 0000000..db5da3b --- /dev/null +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinatesTest.java @@ -0,0 +1,360 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.geometry.euclidean.twod; + +import java.util.regex.Pattern; + +import org.apache.commons.geometry.core.Geometry; +import org.apache.commons.geometry.core.util.Coordinates; +import org.junit.Assert; +import org.junit.Test; + +public class PolarCoordinatesTest { + + private static final double EPS = 1e-10; + + @Test + public void testOf() { + // act/assert + checkPolar(PolarCoordinates.of(0, 0), 0, 0); + + checkPolar(PolarCoordinates.of(2, 0), 2, 0); + checkPolar(PolarCoordinates.of(2, Geometry.HALF_PI), 2, Geometry.HALF_PI); + checkPolar(PolarCoordinates.of(2, Geometry.PI), 2, Geometry.PI); + checkPolar(PolarCoordinates.of(2, Geometry.MINUS_HALF_PI), 2, Geometry.MINUS_HALF_PI); + } + + @Test + public void testOf_unnormalizedAngles() { + // act/assert + checkPolar(PolarCoordinates.of(2, Geometry.TWO_PI), 2, 0); + checkPolar(PolarCoordinates.of(2, Geometry.HALF_PI + Geometry.TWO_PI), 2, Geometry.HALF_PI); + checkPolar(PolarCoordinates.of(2, -Geometry.PI), 2, Geometry.PI); + checkPolar(PolarCoordinates.of(2, Geometry.PI * 1.5), 2, Geometry.MINUS_HALF_PI); + } + + @Test + public void testOf_azimuthWrapAround() { + // arrange + double delta = 1e-6; + + // act/assert + checkAzimuthWrapAround(2, 0); + checkAzimuthWrapAround(2, delta); + checkAzimuthWrapAround(2, Geometry.PI - delta); + checkAzimuthWrapAround(2, Geometry.PI); + + checkAzimuthWrapAround(2, 0); + checkAzimuthWrapAround(2, -delta); + checkAzimuthWrapAround(2, delta - Geometry.PI); + } + + private void checkAzimuthWrapAround(double radius, double azimuth) { + checkPolar(PolarCoordinates.of(radius, azimuth), radius, azimuth); + + checkPolar(PolarCoordinates.of(radius, azimuth - Geometry.TWO_PI), radius, azimuth); + checkPolar(PolarCoordinates.of(radius, azimuth - (2 * Geometry.TWO_PI)), radius, azimuth); + checkPolar(PolarCoordinates.of(radius, azimuth - (3 * Geometry.TWO_PI)), radius, azimuth); + + checkPolar(PolarCoordinates.of(radius, azimuth + Geometry.TWO_PI), radius, azimuth); + checkPolar(PolarCoordinates.of(radius, azimuth + (2 * Geometry.TWO_PI)), radius, azimuth); + checkPolar(PolarCoordinates.of(radius, azimuth + (3 * Geometry.TWO_PI)), radius, azimuth); + } + + @Test + public void testOf_negativeRadius() { + // act/assert + checkPolar(PolarCoordinates.of(-1, 0), 1, Geometry.PI); + checkPolar(PolarCoordinates.of(-1e-6, Geometry.HALF_PI), 1e-6, Geometry.MINUS_HALF_PI); + checkPolar(PolarCoordinates.of(-2, Geometry.PI), 2, 0); + checkPolar(PolarCoordinates.of(-3, Geometry.MINUS_HALF_PI), 3, Geometry.HALF_PI); + } + + @Test + public void testOf_NaNAndInfinite() { + // act/assert + checkPolar(PolarCoordinates.of(Double.NaN, 0), Double.NaN, 0); + checkPolar(PolarCoordinates.of(Double.NEGATIVE_INFINITY, 0), Double.POSITIVE_INFINITY, Geometry.PI); + checkPolar(PolarCoordinates.of(Double.POSITIVE_INFINITY, 0), Double.POSITIVE_INFINITY, 0); + + checkPolar(PolarCoordinates.of(0, Double.NaN), 0, Double.NaN); + checkPolar(PolarCoordinates.of(0, Double.NEGATIVE_INFINITY), 0, Double.NEGATIVE_INFINITY); + checkPolar(PolarCoordinates.of(0, Double.POSITIVE_INFINITY), 0, Double.POSITIVE_INFINITY); + } + + @Test + public void testOfCartesian() { + // arrange + double sqrt2 = Math.sqrt(2); + + // act/assert + checkPolar(PolarCoordinates.ofCartesian(0, 0), 0, 0); + + checkPolar(PolarCoordinates.ofCartesian(1, 0), 1, 0); + checkPolar(PolarCoordinates.ofCartesian(1, 1), sqrt2, 0.25 * Geometry.PI); + checkPolar(PolarCoordinates.ofCartesian(0, 1), 1, Geometry.HALF_PI); + + checkPolar(PolarCoordinates.ofCartesian(-1, 1), sqrt2, 0.75 * Geometry.PI); + checkPolar(PolarCoordinates.ofCartesian(-1, 0), 1, Geometry.PI); + checkPolar(PolarCoordinates.ofCartesian(-1, -1), sqrt2, - 0.75 * Geometry.PI); + + checkPolar(PolarCoordinates.ofCartesian(0, -1), 1, Geometry.MINUS_HALF_PI); + checkPolar(PolarCoordinates.ofCartesian(1, -1), sqrt2, -0.25 * Geometry.PI); + } + + @Test + public void testDimension() { + // arrange + PolarCoordinates p = PolarCoordinates.of(1, 0); + + // act/assert + Assert.assertEquals(2, p.getDimension()); + } + + @Test + public void testIsNaN() { + // act/assert + Assert.assertFalse(PolarCoordinates.of(1, 0).isNaN()); + Assert.assertFalse(PolarCoordinates.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).isNaN()); + + Assert.assertTrue(PolarCoordinates.of(Double.NaN, 0).isNaN()); + Assert.assertTrue(PolarCoordinates.of(1, Double.NaN).isNaN()); + Assert.assertTrue(PolarCoordinates.of(Double.NaN, Double.NaN).isNaN()); + } + + @Test + public void testIsInfinite() { + // act/assert + Assert.assertFalse(PolarCoordinates.of(1, 0).isInfinite()); + Assert.assertFalse(PolarCoordinates.of(Double.NaN, Double.NaN).isInfinite()); + + Assert.assertTrue(PolarCoordinates.of(Double.POSITIVE_INFINITY, 0).isInfinite()); + Assert.assertTrue(PolarCoordinates.of(Double.NEGATIVE_INFINITY, 0).isInfinite()); + Assert.assertFalse(PolarCoordinates.of(Double.NEGATIVE_INFINITY, Double.NaN).isInfinite()); + + Assert.assertTrue(PolarCoordinates.of(0, Double.POSITIVE_INFINITY).isInfinite()); + Assert.assertTrue(PolarCoordinates.of(0, Double.NEGATIVE_INFINITY).isInfinite()); + Assert.assertFalse(PolarCoordinates.of(Double.NaN, Double.NEGATIVE_INFINITY).isInfinite()); + + Assert.assertTrue(PolarCoordinates.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY).isInfinite()); + Assert.assertTrue(PolarCoordinates.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY).isInfinite()); + } + + @Test + public void testHashCode() { + // arrange + PolarCoordinates a = PolarCoordinates.of(1, 2); + PolarCoordinates b = PolarCoordinates.of(10, 2); + PolarCoordinates c = PolarCoordinates.of(10, 20); + PolarCoordinates d = PolarCoordinates.of(1, 20); + + PolarCoordinates e = PolarCoordinates.of(1, 2); + + // act/assert + Assert.assertEquals(a.hashCode(), a.hashCode()); + Assert.assertEquals(a.hashCode(), e.hashCode()); + + Assert.assertNotEquals(a.hashCode(), b.hashCode()); + Assert.assertNotEquals(a.hashCode(), c.hashCode()); + Assert.assertNotEquals(a.hashCode(), d.hashCode()); + } + + @Test + public void testHashCode_NaNInstancesHaveSameHashCode() { + // arrange + PolarCoordinates a = PolarCoordinates.of(1, Double.NaN); + PolarCoordinates b = PolarCoordinates.of(Double.NaN, 1); + + // act/assert + Assert.assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + public void testEquals() { + // arrange + PolarCoordinates a = PolarCoordinates.of(1, 2); + PolarCoordinates b = PolarCoordinates.of(10, 2); + PolarCoordinates c = PolarCoordinates.of(10, 20); + PolarCoordinates d = PolarCoordinates.of(1, 20); + + PolarCoordinates e = PolarCoordinates.of(1, 2); + + // act/assert + Assert.assertFalse(a.equals(null)); + Assert.assertFalse(a.equals(new Object())); + + Assert.assertTrue(a.equals(a)); + Assert.assertTrue(a.equals(e)); + + Assert.assertFalse(a.equals(b)); + Assert.assertFalse(a.equals(c)); + Assert.assertFalse(a.equals(d)); + } + + @Test + public void testEquals_NaNInstancesEqual() { + // arrange + PolarCoordinates a = PolarCoordinates.of(1, Double.NaN); + PolarCoordinates b = PolarCoordinates.of(Double.NaN, 1); + + // act/assert + Assert.assertTrue(a.equals(b)); + } + + @Test + public void testToVector() { + // arrange + double sqrt2 = Math.sqrt(2); + + // act/assert + checkVector(PolarCoordinates.of(0, 0).toVector(), 0, 0); + + checkVector(PolarCoordinates.of(1, 0).toVector(), 1, 0); + checkVector(PolarCoordinates.of(sqrt2, 0.25 * Geometry.PI).toVector(), 1, 1); + checkVector(PolarCoordinates.of(1, Geometry.HALF_PI).toVector(), 0, 1); + + checkVector(PolarCoordinates.of(sqrt2, 0.75 * Geometry.PI).toVector(), -1, 1); + checkVector(PolarCoordinates.of(1, Geometry.PI).toVector(), -1, 0); + checkVector(PolarCoordinates.of(sqrt2, -0.75 * Geometry.PI).toVector(), -1, -1); + + checkVector(PolarCoordinates.of(1, Geometry.MINUS_HALF_PI).toVector(), 0, -1); + checkVector(PolarCoordinates.of(sqrt2, -0.25 * Geometry.PI).toVector(), 1, -1); + } + + @Test + public void testToPoint() { + // arrange + double sqrt2 = Math.sqrt(2); + + // act/assert + checkPoint(PolarCoordinates.of(0, 0).toPoint(), 0, 0); + + checkPoint(PolarCoordinates.of(1, 0).toPoint(), 1, 0); + checkPoint(PolarCoordinates.of(sqrt2, 0.25 * Geometry.PI).toPoint(), 1, 1); + checkPoint(PolarCoordinates.of(1, Geometry.HALF_PI).toPoint(), 0, 1); + + checkPoint(PolarCoordinates.of(sqrt2, 0.75 * Geometry.PI).toPoint(), -1, 1); + checkPoint(PolarCoordinates.of(1, Geometry.PI).toPoint(), -1, 0); + checkPoint(PolarCoordinates.of(sqrt2, -0.75 * Geometry.PI).toPoint(), -1, -1); + + checkPoint(PolarCoordinates.of(1, Geometry.MINUS_HALF_PI).toPoint(), 0, -1); + checkPoint(PolarCoordinates.of(sqrt2, -0.25 * Geometry.PI).toPoint(), 1, -1); + } + + @Test + public void testToCartesian() { + // arrange + Coordinates.Factory2D<Point2D> factory = Point2D.getFactory(); + double sqrt2 = Math.sqrt(2); + + // act/assert + checkPoint(PolarCoordinates.of(0, 0).toCartesian(factory), 0, 0); + + checkPoint(PolarCoordinates.of(1, 0).toCartesian(factory), 1, 0); + checkPoint(PolarCoordinates.of(sqrt2, 0.25 * Geometry.PI).toCartesian(factory), 1, 1); + checkPoint(PolarCoordinates.of(1, Geometry.HALF_PI).toCartesian(factory), 0, 1); + + checkPoint(PolarCoordinates.of(sqrt2, 0.75 * Geometry.PI).toCartesian(factory), -1, 1); + checkPoint(PolarCoordinates.of(1, Geometry.PI).toCartesian(factory), -1, 0); + checkPoint(PolarCoordinates.of(sqrt2, -0.75 * Geometry.PI).toCartesian(factory), -1, -1); + + checkPoint(PolarCoordinates.of(1, Geometry.MINUS_HALF_PI).toCartesian(factory), 0, -1); + checkPoint(PolarCoordinates.of(sqrt2, -0.25 * Geometry.PI).toCartesian(factory), 1, -1); + } + + @Test + public void testToCartesian_static() { + // arrange + Coordinates.Factory2D<Point2D> factory = Point2D.getFactory(); + double sqrt2 = Math.sqrt(2); + + // act/assert + checkPoint(PolarCoordinates.toCartesian(0, 0, factory), 0, 0); + + checkPoint(PolarCoordinates.toCartesian(1, 0, factory), 1, 0); + checkPoint(PolarCoordinates.toCartesian(sqrt2, 0.25 * Geometry.PI, factory), 1, 1); + checkPoint(PolarCoordinates.toCartesian(1, Geometry.HALF_PI, factory), 0, 1); + + checkPoint(PolarCoordinates.toCartesian(sqrt2, 0.75 * Geometry.PI, factory), -1, 1); + checkPoint(PolarCoordinates.toCartesian(1, Geometry.PI, factory), -1, 0); + checkPoint(PolarCoordinates.toCartesian(sqrt2, -0.75 * Geometry.PI, factory), -1, -1); + + checkPoint(PolarCoordinates.toCartesian(1, Geometry.MINUS_HALF_PI, factory), 0, -1); + checkPoint(PolarCoordinates.toCartesian(sqrt2, -0.25 * Geometry.PI, factory), 1, -1); + } + + @Test + public void testToCartesian_static_NaNAndInfinite() { + // arrange + Coordinates.Factory2D<Point2D> factory = Point2D.getFactory(); + + // act/assert + Assert.assertTrue(PolarCoordinates.toCartesian(Double.NaN, 0, factory).isNaN()); + Assert.assertTrue(PolarCoordinates.toCartesian(0, Double.NaN, factory).isNaN()); + + Assert.assertTrue(PolarCoordinates.toCartesian(Double.POSITIVE_INFINITY, 0, factory).isNaN()); + Assert.assertTrue(PolarCoordinates.toCartesian(0, Double.POSITIVE_INFINITY, factory).isNaN()); + Assert.assertTrue(PolarCoordinates.toCartesian(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, factory).isNaN()); + + Assert.assertTrue(PolarCoordinates.toCartesian(Double.NEGATIVE_INFINITY, 0, factory).isNaN()); + Assert.assertTrue(PolarCoordinates.toCartesian(0, Double.NEGATIVE_INFINITY, factory).isNaN()); + Assert.assertTrue(PolarCoordinates.toCartesian(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, factory).isNaN()); + } + + @Test + public void testToString() { + // arrange + PolarCoordinates polar = PolarCoordinates.of(1, 2); + Pattern pattern = Pattern.compile("\\(r=1.{0,2}, az=2.{0,2}\\)"); + + // act + String str = polar.toString();; + + // assert + Assert.assertTrue("Expected string " + str + " to match regex " + pattern, + pattern.matcher(str).matches()); + } + + @Test + public void testParse() { + // act/assert + checkPolar(PolarCoordinates.parse("(r=1, az=2)"), 1, 2); + checkPolar(PolarCoordinates.parse("( r= -1, az= 0.5 )"), 1, 0.5 - Geometry.PI); + checkPolar(PolarCoordinates.parse("( r=NaN, az= -Infinity )"), Double.NaN, Double.NEGATIVE_INFINITY); + } + + @Test(expected = IllegalArgumentException.class) + public void testParse_failure() { + // act/assert + PolarCoordinates.parse("abc"); + } + + private void checkPolar(PolarCoordinates polar, double radius, double azimuth) { + Assert.assertEquals(radius, polar.getRadius(), EPS); + Assert.assertEquals(azimuth, polar.getAzimuth(), EPS); + } + + private void checkVector(Vector2D v, double x, double y) { + Assert.assertEquals(x, v.getX(), EPS); + Assert.assertEquals(y, v.getY(), EPS); + } + + private void checkPoint(Point2D p, double x, double y) { + Assert.assertEquals(x, p.getX(), EPS); + Assert.assertEquals(y, p.getY(), EPS); + } +} 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 13208c0..dbd2a2a 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 @@ -485,6 +485,28 @@ public class Vector2DTest { } @Test + public void testOfPolar() { + // arrange + double eps = 1e-15; + double sqrt2 = Math.sqrt(2.0); + + // act/assert + checkVector(Vector2D.ofPolar(0, 0), 0, 0, eps); + checkVector(Vector2D.ofPolar(1, 0), 1, 0, eps); + + checkVector(Vector2D.ofPolar(2, Geometry.PI), -2, 0, eps); + checkVector(Vector2D.ofPolar(-2, Geometry.PI), 2, 0, eps); + + checkVector(Vector2D.ofPolar(2, Geometry.HALF_PI), 0, 2, eps); + checkVector(Vector2D.ofPolar(-2, Geometry.HALF_PI), 0, -2, eps); + + checkVector(Vector2D.ofPolar(2, 0.25 * Geometry.PI), sqrt2, sqrt2, eps); + checkVector(Vector2D.ofPolar(2, 0.75 * Geometry.PI), -sqrt2, sqrt2, eps); + checkVector(Vector2D.ofPolar(2, -0.25 * Geometry.PI), sqrt2, - sqrt2, eps); + checkVector(Vector2D.ofPolar(2, -0.75 * Geometry.PI), -sqrt2, - sqrt2, eps); + } + + @Test public void testGetFactory() { // act Coordinates.Factory2D<Vector2D> factory = Vector2D.getFactory(); @@ -546,8 +568,12 @@ public class Vector2DTest { } private void checkVector(Vector2D v, double x, double y) { - Assert.assertEquals(x, v.getX(), EPS); - Assert.assertEquals(y, v.getY(), EPS); + checkVector(v, x, y, EPS); + } + + private void checkVector(Vector2D v, double x, double y, double eps) { + Assert.assertEquals(x, v.getX(), eps); + Assert.assertEquals(y, v.getY(), eps); } private void checkPoint(Point2D p, double x, double y) {
