asfgit closed pull request #7: Polar and Spherical Coordinates URL: https://github.com/apache/commons-geometry/pull/7
This is a PR merged from a forked repository. As GitHub hides the original diff on merge, it is displayed below for the sake of provenance: As this is a foreign pull request (from a fork), the diff is supplied below (as it won't show otherwise due to GitHub magic): 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..fe319c4 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 @@ -18,23 +18,29 @@ /** Class containing geometric constants. */ -public class Geometry { +public final 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; + /** Constant value for {@code 3*pi/2}. */ + public static final double THREE_HALVES_PI = 1.5 * Math.PI; + /** Private constructor */ private Geometry() {} } 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..61fc872 --- /dev/null +++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTest.java @@ -0,0 +1,66 @@ +/* + * 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); + + Assert.assertEquals((3.0 * Math.PI) / 2.0, Geometry.THREE_HALVES_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/threed/Cartesian3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java index 6dbe753..f8c07b9 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java @@ -78,6 +78,13 @@ public double getZ() { return new double[] { x, y, z }; } + /** Return an equivalent set of coordinates in spherical form. + * @return an equivalent set of coordinates in spherical form. + */ + public SphericalCoordinates toSpherical() { + return SphericalCoordinates.ofCartesian(x, y, z); + } + /** {@inheritDoc} */ @Override public int getDimension() { diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java index 7c2b3f7..e099130 100644 --- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java @@ -43,11 +43,8 @@ public static final Point3D NEGATIVE_INFINITY = new Point3D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); - /** Serializable version identifier. */ - private static final long serialVersionUID = 20180710L; - - /** Factory for delegating instance creation. */ - private static DoubleFunction3N<Point3D> FACTORY = new DoubleFunction3N<Point3D>() { + /** Package private factory for delegating instance creation. */ + static final DoubleFunction3N<Point3D> FACTORY = new DoubleFunction3N<Point3D>() { /** {@inheritDoc} */ @Override @@ -56,6 +53,9 @@ public Point3D apply(double n1, double n2, double n3) { } }; + /** Serializable version identifier. */ + private static final long serialVersionUID = 20180710L; + /** Simple constructor. * Build a point from its coordinates * @param x abscissa @@ -184,6 +184,17 @@ public static Point3D of(double[] p) { return new Point3D(p[0], p[1], p[2]); } + /** Create a point from a set of spherical coordinates. + * @param radius the spherical radius value + * @param azimuth the angle in the x-y plane measured in radians counter-clockwise from the + * positive x axis. + * @param polar the angle with the positive z axis in radians. + * @return a point instance with the given set of spherical coordinates + */ + public static Point3D ofSpherical(double radius, double azimuth, double polar) { + return SphericalCoordinates.toCartesian(radius, azimuth, polar, FACTORY); + } + /** 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/threed/SphericalCoordinates.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinates.java new file mode 100644 index 0000000..86b664c --- /dev/null +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinates.java @@ -0,0 +1,312 @@ +/* + * 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.threed; + +import java.io.Serializable; + +import org.apache.commons.geometry.core.Geometry; +import org.apache.commons.geometry.core.Spatial; +import org.apache.commons.geometry.core.internal.DoubleFunction3N; +import org.apache.commons.geometry.core.internal.SimpleTupleFormat; +import org.apache.commons.geometry.euclidean.twod.PolarCoordinates; +import org.apache.commons.numbers.angle.PlaneAngleRadians; + +/** Class representing <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">spherical coordinates</a> + * in 3 dimensional Euclidean space. + * + * <p>Spherical coordinates for a point are defined by three values: + * <ol> + * <li><em>Radius</em> - The distance from the point to a fixed referenced point.</li> + * <li><em>Azimuth angle</em> - The angle measured from a fixed reference direction in a plane to + * the orthogonal projection of the point on that plane.</li> + * <li><em>Polar angle</em> - The angle measured from a fixed zenith direction to the point. The zenith + *direction must be orthogonal to the reference plane.</li> + * </ol> + * This class follows the convention of using the origin as the reference point; the positive x-axis as the + * reference direction for the azimuth angle, measured in the x-y plane with positive angles moving counter-clockwise + * toward the positive y-axis; and the positive z-axis as the zenith direction. Spherical coordinates are + * related to Cartesian coordinates as follows: + * <pre> + * x = r cos(θ) sin(Φ) + * y = r sin(θ) sin(Φ) + * z = r cos(Φ) + * + * r = √(x^2 + y^2 + z^2) + * θ = atan2(y, x) + * Φ = acos(z/r) + * </pre> + * where <em>r</em> is the radius, <em>θ</em> is the azimuth angle, and <em>Φ</em> is the polar angle + * of the spherical coordinates. + * + * <p>There are numerous, competing conventions for the symbols used to represent spherical coordinate values. For + * example, the mathematical convention is to use <em>(r, θ, Φ)</em> to represent radius, azimuth angle, and + * polar angle, whereas the physics convention flips the angle values and uses <em>(r, Φ, θ)</em>. As such, + * this class avoids the use of these symbols altogether in favor of the less ambiguous formal names of the values, + * e.g. {@code radius}, {@code azimuth}, and {@code polar}.</p> + * + * <p>In order to ensure the uniqueness of coordinate sets, coordinate values + * are normalized so that {@code radius} is in the range {@code [0, +Infinity)}, + * {@code azimuth} is in the range {@code [0, 2pi)}, and {@code polar} is in the + * range {@code [0, pi]}.</p> + * + * @see <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">Spherical Coordinate System</a> + */ +public final class SphericalCoordinates implements Spatial, Serializable { + + /** Serializable version identifier. */ + private static final long serialVersionUID = 20180623L; + + /** Factory object for delegating instance creation. */ + private static final DoubleFunction3N<SphericalCoordinates> FACTORY = new DoubleFunction3N<SphericalCoordinates>() { + + /** {@inheritDoc} */ + @Override + public SphericalCoordinates apply(double n1, double n2, double n3) { + return new SphericalCoordinates(n1, n2, n3); + } + }; + + /** Radius value. */ + private final double radius; + + /** Azimuth angle in radians. */ + private final double azimuth; + + /** Polar angle in radians. */ + private final double polar; + + /** Simple constructor. The given inputs are normalized. + * @param radius Radius value. + * @param azimuth Azimuth angle in radians. + * @param polar Polar angle in radians. + */ + private SphericalCoordinates(double radius, double azimuth, double polar) { + if (radius < 0) { + // negative radius; flip the angles + radius = Math.abs(radius); + azimuth += Geometry.PI; + polar += Geometry.PI; + } + + this.radius = radius; + this.azimuth = normalizeAzimuth(azimuth); + this.polar = normalizePolar(polar); + } + + /** Return the radius value. The value is in the range {@code [0, +Infinity)}. + * @return the radius value + */ + public double getRadius() { + return radius; + } + + /** Return the azimuth angle in radians. This is the angle in the x-y plane measured counter-clockwise from + * the positive x axis. The angle is in the range {@code [0, 2pi)}. + * @return the azimuth angle in radians + */ + public double getAzimuth() { + return azimuth; + } + + /** Return the polar angle in radians. This is the angle the coordinate ray makes with the positive z axis. + * The angle is in the range {@code [0, pi]}. + * @return the polar angle in radians + */ + public double getPolar() { + return polar; + } + + /** {@inheritDoc} */ + @Override + public int getDimension() { + return 3; + } + + /** {@inheritDoc} */ + @Override + public boolean isNaN() { + return Double.isNaN(radius) || Double.isNaN(azimuth) || Double.isNaN(polar); + } + + /** {@inheritDoc} */ + @Override + public boolean isInfinite() { + return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth) || Double.isInfinite(polar)); + } + + /** Convert this set of spherical coordinates to a 3 dimensional vector. + * @return A 3-dimensional vector with an equivalent set of + * coordinates. + */ + public Vector3D toVector() { + return toCartesian(radius, azimuth, polar, Vector3D.FACTORY); + } + + /** Convert this set of spherical coordinates to a 3 dimensional point. + * @return A 3-dimensional point with an equivalent set of + * coordinates. + */ + public Point3D toPoint() { + return toCartesian(radius, azimuth, polar, Point3D.FACTORY); + } + + /** Get a hashCode for this set of spherical 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 127; + } + return 449 * (79 * Double.hashCode(radius) + Double.hashCode(azimuth) + Double.hashCode(polar)); + } + + /** Test for the equality of two sets of spherical 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 any (or all) values of the + * coordinate set are equal to <code>Double.NaN</code>, the set as a whole + * is considered to equal NaN. + * </p> + * + * @param other Object to test for equality to this + * @return true if two SphericalCoordinates objects are equal, false if + * object is null, not an instance of SphericalCoordinates, or + * not equal to this SphericalCoordinates instance + * + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other instanceof SphericalCoordinates) { + final SphericalCoordinates rhs = (SphericalCoordinates) other; + if (rhs.isNaN()) { + return this.isNaN(); + } + + return (radius == rhs.radius) && (azimuth == rhs.azimuth) && (polar == rhs.polar); + } + return false; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return SimpleTupleFormat.getDefault().format(radius, azimuth, polar); + } + + /** Return a new instance with the given spherical coordinate values. The values are normalized + * so that {@code radius} lies in the range {@code [0, +Infinity)}, {@code azimuth} lies in the range + * {@code [0, 2pi)}, and {@code polar} lies in the range {@code [0, +pi]}. + * @param radius the length of the line segment from the origin to the coordinate point. + * @param azimuth the angle in the x-y plane, measured in radians counter-clockwise + * from the positive x-axis. + * @param polar the angle in radians between the positive z-axis and the ray from the origin + * to the coordinate point. + * @return a new {@link SphericalCoordinates} instance representing the same point as the given set of + * spherical coordinates. + */ + public static SphericalCoordinates of(final double radius, final double azimuth, final double polar) { + return new SphericalCoordinates(radius, azimuth, polar); + } + + /** Convert the given set of Cartesian coordinates to spherical coordinates. + * @param x X coordinate value + * @param y Y coordinate value + * @param z Z coordinate value + * @return a set of spherical coordinates equivalent to the given Cartesian coordinates + */ + public static SphericalCoordinates ofCartesian(final double x, final double y, final double z) { + final double radius = Math.sqrt((x*x) + (y*y) + (z*z)); + final double azimuth = Math.atan2(y, x); + + // default the polar angle to 0 when the radius is 0 + final double polar = (radius > 0.0) ? Math.acos(z / radius) : 0.0; + + return new SphericalCoordinates(radius, azimuth, polar); + } + + /** Parse the given string and return a new {@link SphericalCoordinates} instance. The parsed + * coordinate values are normalized as in the {@link #of(double, double, double)} method. + * The expected string format is the same as that returned by {@link #toString()}. + * @param input the string to parse + * @return new {@link SphericalCoordinates} instance + * @throws IllegalArgumentException if the string format is invalid. + */ + public static SphericalCoordinates parse(String input) { + return SimpleTupleFormat.getDefault().parse(input, FACTORY); + } + + /** Normalize an azimuth value to be within the range {@code [0, 2pi)}. This + * is exactly equivalent to {@link PolarCoordinates#normalizeAzimuth(double)}. + * @param azimuth azimuth value in radians + * @return equivalent azimuth value in the range {@code [0, 2pi)}. + * @see PolarCoordinates#normalizeAzimuth(double) + */ + public static double normalizeAzimuth(double azimuth) { + return PolarCoordinates.normalizeAzimuth(azimuth); + } + + /** Normalize a polar value to be within the range {@code [0, +pi]}. Since the + * polar angle is the angle between two vectors (the zenith direction and the + * point vector), the sign of the angle is not significant as in the azimuth angle. + * For example, a polar angle of {@code -pi/2} and one of {@code +pi/2} will both + * normalize to {@code pi/2}. + * @param polar polar value in radians + * @return equalivalent polar value in the range {@code [0, +pi]} + */ + public static double normalizePolar(double polar) { + // normalize the polar angle; this is the angle between the polar vector and the point ray + // so it is unsigned (unlike the azimuth) and should be in the range [0, pi] + if (Double.isFinite(polar)) { + polar = Math.abs(PlaneAngleRadians.normalizeBetweenMinusPiAndPi(polar)); + } + + return polar; + } + + /** Package private method to convert the given set of spherical coordinates to + * Cartesian coordinates. The Cartesian coordinates are computed and passed to the given + * factory instance. The factory's return value is returned. + * @param <T> Factory return type. + * @param radius The spherical radius value. + * @param azimuth The spherical azimuth angle in radians. + * @param polar The spherical polar angle in radians. + * @param factory Factory instance that will be passed the + * @return the value returned by the factory when passed Cartesian + * coordinates equivalent to the given set of spherical coordinates. + */ + static <T> T toCartesian(final double radius, final double azimuth, final double polar, + DoubleFunction3N<T> factory) { + final double xyLength = radius * Math.sin(polar); + + final double x = xyLength * Math.cos(azimuth); + final double y = xyLength * Math.sin(azimuth); + final double z = radius * Math.cos(polar); + + return factory.apply(x, y, z); + } +} 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 459b014..078a5d3 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 @@ -47,7 +47,7 @@ /** Opposite of the third canonical vector (coordinates: 0, 0, -1). */ public static final Vector3D MINUS_Z = new Vector3D(0, 0, -1); - // CHECKSTYLE: stop ConstantName + // CHECKSTYLE: stop ConstantName /** A vector with all coordinates set to NaN. */ public static final Vector3D NaN = new Vector3D(Double.NaN, Double.NaN, Double.NaN); // CHECKSTYLE: resume ConstantName @@ -60,14 +60,8 @@ public static final Vector3D NEGATIVE_INFINITY = new Vector3D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); - /** Serializable UID */ - private static final long serialVersionUID = 20180710L; - - /** Error message when norms are zero. */ - private static final String ZERO_NORM_MSG = "Norm is zero"; - - /** Factory for delegating instance creation. */ - private static DoubleFunction3N<Vector3D> FACTORY = new DoubleFunction3N<Vector3D>() { + /** Package private factory for delegating instance creation. */ + static final DoubleFunction3N<Vector3D> FACTORY = new DoubleFunction3N<Vector3D>() { /** {@inheritDoc} */ @Override @@ -76,6 +70,12 @@ public Vector3D apply(double n1, double n2, double n3) { } }; + /** Serializable UID */ + private static final long serialVersionUID = 20180710L; + + /** Error message when norms are zero. */ + private static final String ZERO_NORM_MSG = "Norm is zero"; + /** Simple constructor. * Build a vector from its coordinates * @param x abscissa @@ -130,20 +130,6 @@ public double getNormInf() { return Math.max(Math.max(Math.abs(getX()), Math.abs(getY())), Math.abs(getZ())); } - /** Get the azimuth of the vector. - * @return azimuth (α) of the vector, between -π and +π - */ - public double getAlpha() { - return Math.atan2(getY(), getX()); - } - - /** Get the elevation of the vector. - * @return elevation (δ) of the vector, between -π/2 and +π/2 - */ - public double getDelta() { - return Math.asin(getZ() / getNorm()); - } - /** {@inheritDoc} */ @Override public Vector3D add(Vector3D v) { @@ -445,21 +431,15 @@ public static Vector3D of(double[] v) { return new Vector3D(v[0], v[1], v[2]); } - /** Builds a vector from its azimuthal coordinates - * @param alpha azimuth (α) around Z - * (0 is +X, π/2 is +Y, π is -X and 3π/2 is -Y) - * @param delta elevation (δ) above (XY) plane, from -π/2 to +π/2 - * @see #getAlpha() - * @see #getDelta() - * @return new vector instance with the given azimuthal coordinates + /** Create a vector from a set of spherical coordinates. + * @param radius the spherical radius value + * @param azimuth the angle in the x-y plane measured in radians counter-clockwise from the + * positive x axis. + * @param polar the angle with the positive z axis in radians. + * @return a vector instance with the given set of spherical coordinates */ - public static Vector3D fromSpherical(double alpha, double delta) { - double cosDelta = Math.cos(delta); - double x = Math.cos(alpha) * cosDelta; - double y = Math.sin(alpha) * cosDelta; - double z = Math.sin(delta); - - return new Vector3D(x, y, z); + public static Vector3D ofSpherical(double radius, double azimuth, double polar) { + return SphericalCoordinates.toCartesian(radius, azimuth, polar, FACTORY); } /** Parses the given string and returns a new vector instance. The expected string 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 8d35fed..7ed4778 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 @@ -60,6 +60,13 @@ public double getY() { 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 */ @@ -96,8 +103,8 @@ public String toString() { * @return Euclidean distance */ protected double euclideanDistance(Cartesian2D other) { - double dx = x - other.x; - double dy = y - other.y; - return Math.sqrt((dx * dx) + (dy * dy)); + final double dx = x - other.x; + final double dy = y - other.y; + return Math.hypot(dx, dy); } } 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 c9fde1f..94c2272 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 @@ -42,11 +42,8 @@ public static final Point2D NEGATIVE_INFINITY = new Point2D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); - /** Serializable UID. */ - private static final long serialVersionUID = 20180710L; - - /** Factory for delegating instance creation. */ - private static DoubleFunction2N<Point2D> FACTORY = new DoubleFunction2N<Point2D>() { + /** Package private factory for delegating instance creation. */ + static final DoubleFunction2N<Point2D> FACTORY = new DoubleFunction2N<Point2D>() { /** {@inheritDoc} */ @Override @@ -55,6 +52,9 @@ public Point2D apply(double n1, double n2) { } }; + /** Serializable UID. */ + private static final long serialVersionUID = 20180710L; + /** Simple constructor. * Build a point from its coordinates * @param x abscissa @@ -172,6 +172,15 @@ public static Point2D of(double[] p) { 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, FACTORY); + } + /** 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..c910629 --- /dev/null +++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java @@ -0,0 +1,262 @@ +/* + * 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 org.apache.commons.geometry.core.Geometry; +import org.apache.commons.geometry.core.Spatial; +import org.apache.commons.geometry.core.internal.DoubleFunction2N; +import org.apache.commons.geometry.core.internal.SimpleTupleFormat; +import org.apache.commons.numbers.angle.PlaneAngleRadians; + +/** Class representing <a href="https://en.wikipedia.org/wiki/Polar_coordinate_system">polar coordinates</a> + * in 2 dimensional Euclidean space. + * + * <p>Polar coordinates are defined by a distance from a reference point + * and an angle from a reference direction. The distance value is called + * the radial coordinate, or <em>radius</em>, and the angle is called the angular coordinate, + * or <em>azimuth</em>. This class follows the standard + * mathematical convention of using the positive x-axis as the reference + * direction and measuring positive angles counter-clockwise, toward the + * positive y-axis. The origin is used as the reference point. Polar coordinate + * are related to Cartesian coordinates as follows: + * <pre> + * x = r * cos(θ) + * y = r * sin(θ) + * + * r = √(x^2 + y^2) + * θ = atan2(y, x) + * </pre> + * where <em>r</em> is the radius and <em>θ</em> is the azimuth of the polar coordinates. + * + * <p>In order to ensure the uniqueness of coordinate sets, coordinate values + * are normalized so that {@code radius} is in the range {@code [0, +Infinity)} + * and {@code azimuth} is in the range {@code [0, 2pi)}.</p> + * + * @see <a href="https://en.wikipedia.org/wiki/Polar_coordinate_system">Polar Coordinate System</a> + */ +public final class PolarCoordinates implements Spatial, Serializable { + + /** Serializable version UID */ + private static final long serialVersionUID = 20180630L; + + /** Factory object for delegating instance creation. */ + private static final DoubleFunction2N<PolarCoordinates> FACTORY = new DoubleFunction2N<PolarCoordinates>() { + + /** {@inheritDoc} */ + @Override + public PolarCoordinates apply(double n1, double n2) { + return new PolarCoordinates(n1, n2); + } + }; + + /** Radius value */ + private final double radius; + + /** Azimuth angle in radians */ + private final double azimuth; + + /** Simple constructor. Input values are normalized. + * @param radius Radius value. + * @param azimuth Azimuth angle in radians. + */ + private PolarCoordinates(double radius, double azimuth) { + if (radius < 0) { + // negative radius; flip the angles + radius = Math.abs(radius); + azimuth += Geometry.PI; + } + + this.radius = radius; + this.azimuth = normalizeAzimuth(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 [0, 2pi)}. + * @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 a 2-dimensional + * vector. + * @return A 2-dimensional vector with an equivalent set of + * coordinates. + */ + public Vector2D toVector() { + return toCartesian(radius, azimuth, Vector2D.FACTORY); + } + + /** 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(radius, azimuth, Point2D.FACTORY); + } + + /** 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 as a whole is + * considered to equal <code>NaN</code>. + * </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 SimpleTupleFormat.getDefault().format(radius, azimuth); + } + + /** Return a new instance with the given polar coordinate values. + * The values are normalized so that {@code radius} lies in the range {@code [0, +Infinity)} + * and {@code azimuth} in the range {@code [0, 2pi)}. + * @param radius Radius value. + * @param azimuth Azimuth angle in radians. + * @return new {@link PolarCoordinates} instance + */ + public static PolarCoordinates of(double radius, double azimuth) { + 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 as in the {@link #of(double, double)} method. The expected string + * format is the same as that returned by {@link #toString()}. + * @param input the string to parse + * @return new {@link PolarCoordinates} instance + * @throws IllegalArgumentException if the string format is invalid. + */ + public static PolarCoordinates parse(String input) { + return SimpleTupleFormat.getDefault().parse(input, FACTORY); + } + + /** Normalize an azimuth value to be within the range {@code [0, 2pi)}. + * @param azimuth azimuth value in radians + * @return equivalent azimuth value in the range {@code [0, 2pi)}. + */ + public static double normalizeAzimuth(double azimuth) { + if (Double.isFinite(azimuth) && (azimuth < 0.0 || azimuth >= Geometry.TWO_PI)) { + azimuth = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(azimuth); + + // azimuth is now in the range [0, 2pi] but we want it to be in the range + // [0, 2pi) in order to have completely unique coordinates + if (azimuth >= Geometry.TWO_PI) { + azimuth -= Geometry.TWO_PI; + } + } + + return azimuth; + } + + /** Package private method to 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 factory when passed Cartesian + * coordinates equivalent to the given set of polar coordinates. + */ + static <T> T toCartesian(final double radius, final double azimuth, final DoubleFunction2N<T> factory) { + final double x = radius * Math.cos(azimuth); + final double y = radius * Math.sin(azimuth); + + return factory.apply(x, y); + } +} 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 4b435cc..36c300e 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 @@ -54,14 +54,8 @@ public static final Vector2D NEGATIVE_INFINITY = new Vector2D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); - /** Serializable UID */ - private static final long serialVersionUID = 20180710L; - - /** Error message when norms are zero. */ - private static final String ZERO_NORM_MSG = "Norm is zero"; - - /** Factory for delegating instance creation. */ - private static DoubleFunction2N<Vector2D> FACTORY = new DoubleFunction2N<Vector2D>() { + /** Package private factory for delegating instance creation. */ + static final DoubleFunction2N<Vector2D> FACTORY = new DoubleFunction2N<Vector2D>() { /** {@inheritDoc} */ @Override @@ -70,6 +64,12 @@ public Vector2D apply(double n1, double n2) { } }; + /** Serializable UID */ + private static final long serialVersionUID = 20180710L; + + /** Error message when norms are zero. */ + private static final String ZERO_NORM_MSG = "Norm is zero"; + /** Simple constructor. * @param x abscissa (first coordinate) * @param y ordinate (second coordinate) @@ -371,6 +371,15 @@ public static Vector2D of(double[] v) { 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, Vector2D.FACTORY); + } + /** 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/threed/Cartesian3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Cartesian3DTest.java index aadfe73..6e075b9 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Cartesian3DTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Cartesian3DTest.java @@ -2,6 +2,7 @@ import java.util.regex.Pattern; +import org.apache.commons.geometry.core.Geometry; import org.junit.Assert; import org.junit.Test; @@ -35,6 +36,28 @@ public void testToArray() { Assert.assertEquals(3.0, arr[2], TEST_TOLERANCE); } + + @Test + public void testToSpherical() { + // arrange + double sqrt3 = Math.sqrt(3); + + // act/assert + checkSpherical(new StubCartesian3D(0, 0, 0).toSpherical(), 0, 0, 0); + + checkSpherical(new StubCartesian3D(0.1, 0, 0).toSpherical(), 0.1, 0, Geometry.HALF_PI); + checkSpherical(new StubCartesian3D(-0.1, 0, 0).toSpherical(), 0.1, Geometry.PI, Geometry.HALF_PI); + + checkSpherical(new StubCartesian3D(0, 0.1, 0).toSpherical(), 0.1, Geometry.HALF_PI, Geometry.HALF_PI); + checkSpherical(new StubCartesian3D(0, -0.1, 0).toSpherical(), 0.1, Geometry.PI + Geometry.HALF_PI, Geometry.HALF_PI); + + checkSpherical(new StubCartesian3D(0, 0, 0.1).toSpherical(), 0.1, 0, 0); + checkSpherical(new StubCartesian3D(0, 0, -0.1).toSpherical(), 0.1, 0, Geometry.PI); + + checkSpherical(new StubCartesian3D(1, 1, 1).toSpherical(), sqrt3, 0.25 * Geometry.PI, Math.acos(1 / sqrt3)); + checkSpherical(new StubCartesian3D(-1, -1, -1).toSpherical(), sqrt3, 1.25 * Geometry.PI, Math.acos(-1 / sqrt3)); + } + @Test public void testDimension() { // arrange @@ -89,6 +112,12 @@ public void testToString() { pattern.matcher(str).matches()); } + private void checkSpherical(SphericalCoordinates c, double radius, double azimuth, double polar) { + Assert.assertEquals(radius, c.getRadius(), TEST_TOLERANCE); + Assert.assertEquals(azimuth, c.getAzimuth(), TEST_TOLERANCE); + Assert.assertEquals(polar, c.getPolar(), TEST_TOLERANCE); + } + private static class StubCartesian3D extends Cartesian3D { private static final long serialVersionUID = 1L; diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Point3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Point3DTest.java index 62218a6..b76cbda 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Point3DTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Point3DTest.java @@ -18,13 +18,14 @@ import java.util.regex.Pattern; +import org.apache.commons.geometry.core.Geometry; import org.apache.commons.numbers.core.Precision; import org.junit.Assert; import org.junit.Test; public class Point3DTest { - private static final double EPS = Math.ulp(1d); + private static final double EPS = 1e-15; @Test public void testConstants() { @@ -241,6 +242,27 @@ public void testOf_arrayArg_invalidDimensions() { Point3D.of(new double[] { 0.0, 0.0 }); } + @Test + public void testOfSpherical() { + // arrange + double sqrt3 = Math.sqrt(3); + + // act/assert + checkPoint(Point3D.ofSpherical(0, 0, 0), 0, 0, 0); + + checkPoint(Point3D.ofSpherical(1, 0, Geometry.HALF_PI), 1, 0, 0); + checkPoint(Point3D.ofSpherical(1, Geometry.PI, Geometry.HALF_PI), -1, 0, 0); + + checkPoint(Point3D.ofSpherical(2, Geometry.HALF_PI, Geometry.HALF_PI), 0, 2, 0); + checkPoint(Point3D.ofSpherical(2, Geometry.MINUS_HALF_PI, Geometry.HALF_PI), 0, -2, 0); + + checkPoint(Point3D.ofSpherical(3, 0, 0), 0, 0, 3); + checkPoint(Point3D.ofSpherical(3, 0, Geometry.PI), 0, 0, -3); + + checkPoint(Point3D.ofSpherical(sqrt3, 0.25 * Geometry.PI, Math.acos(1 / sqrt3)), 1, 1, 1); + checkPoint(Point3D.ofSpherical(sqrt3, -0.75 * Geometry.PI, Math.acos(-1 / sqrt3)), -1, -1, -1); + } + @Test public void testVectorCombination1() { // arrange diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinatesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinatesTest.java new file mode 100644 index 0000000..ce4f03f --- /dev/null +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SphericalCoordinatesTest.java @@ -0,0 +1,399 @@ +/* + * 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.threed; + +import java.util.regex.Pattern; + +import org.apache.commons.geometry.core.Geometry; +import org.apache.commons.geometry.core.internal.DoubleFunction3N; +import org.junit.Assert; +import org.junit.Test; + +public class SphericalCoordinatesTest { + + private static final double EPS = 1e-10; + + private static final double QUARTER_PI = 0.25 * Geometry.PI; + private static final double MINUS_QUARTER_PI = -0.25 * Geometry.PI; + private static final double THREE_QUARTER_PI = 0.75 * Geometry.PI; + private static final double MINUS_THREE_QUARTER_PI = -0.75 * Geometry.PI; + + @Test + public void testOf() { + // act/assert + checkSpherical(SphericalCoordinates.of(0, 0, 0), 0, 0, 0); + checkSpherical(SphericalCoordinates.of(0.1, 0.2, 0.3), 0.1, 0.2, 0.3); + + checkSpherical(SphericalCoordinates.of(1, Geometry.HALF_PI, Geometry.PI), + 1, Geometry.HALF_PI, Geometry.PI); + checkSpherical(SphericalCoordinates.of(1, Geometry.MINUS_HALF_PI, Geometry.HALF_PI), + 1, Geometry.THREE_HALVES_PI, Geometry.HALF_PI); + } + + @Test + public void testOf_normalizesAzimuthAngle() { + // act/assert + checkSpherical(SphericalCoordinates.of(2, Geometry.TWO_PI, 0), 2, 0, 0); + checkSpherical(SphericalCoordinates.of(2, Geometry.HALF_PI + Geometry.TWO_PI, 0), 2, Geometry.HALF_PI, 0); + checkSpherical(SphericalCoordinates.of(2, -Geometry.PI, 0), 2, Geometry.PI, 0); + checkSpherical(SphericalCoordinates.of(2, Geometry.THREE_HALVES_PI, 0), 2, Geometry.THREE_HALVES_PI, 0); + } + + @Test + public void testOf_normalizesPolarAngle() { + // act/assert + checkSpherical(SphericalCoordinates.of(1, 0, 0), 1, 0, 0); + + checkSpherical(SphericalCoordinates.of(1, 0, QUARTER_PI), 1, 0, QUARTER_PI); + checkSpherical(SphericalCoordinates.of(1, 0, MINUS_QUARTER_PI), 1, 0, QUARTER_PI); + + checkSpherical(SphericalCoordinates.of(1, 0, Geometry.HALF_PI), 1, 0, Geometry.HALF_PI); + checkSpherical(SphericalCoordinates.of(1, 0, Geometry.MINUS_HALF_PI), 1, 0, Geometry.HALF_PI); + + checkSpherical(SphericalCoordinates.of(1, 0, THREE_QUARTER_PI), 1, 0, THREE_QUARTER_PI); + checkSpherical(SphericalCoordinates.of(1, 0, MINUS_THREE_QUARTER_PI), 1, 0, THREE_QUARTER_PI); + + checkSpherical(SphericalCoordinates.of(1, 0, Geometry.TWO_PI), 1, 0, 0); + checkSpherical(SphericalCoordinates.of(1, 0, Geometry.MINUS_TWO_PI), 1, 0, 0); + } + + @Test + public void testOf_angleWrapAround() { + // act/assert + checkOfWithAngleWrapAround(1, 0, 0); + checkOfWithAngleWrapAround(1, QUARTER_PI, QUARTER_PI); + checkOfWithAngleWrapAround(1, Geometry.HALF_PI, Geometry.HALF_PI); + checkOfWithAngleWrapAround(1, THREE_QUARTER_PI, THREE_QUARTER_PI); + checkOfWithAngleWrapAround(1, Geometry.PI, Geometry.PI); + } + + private void checkOfWithAngleWrapAround(double radius, double azimuth, double polar) { + for (int i=-4; i<=4; ++i) { + checkSpherical( + SphericalCoordinates.of(radius, azimuth + (i * Geometry.TWO_PI), polar + (-i * Geometry.TWO_PI)), + radius, azimuth, polar); + } + } + + @Test + public void testOf_negativeRadius() { + // act/assert + checkSpherical(SphericalCoordinates.of(-2, 0, 0), 2, Geometry.PI, Geometry.PI); + checkSpherical(SphericalCoordinates.of(-2, Geometry.PI, Geometry.PI), 2, 0, 0); + + checkSpherical(SphericalCoordinates.of(-3, Geometry.HALF_PI, QUARTER_PI), 3, Geometry.THREE_HALVES_PI, THREE_QUARTER_PI); + checkSpherical(SphericalCoordinates.of(-3, Geometry.MINUS_HALF_PI, THREE_QUARTER_PI), 3, Geometry.HALF_PI, QUARTER_PI); + + checkSpherical(SphericalCoordinates.of(-4, QUARTER_PI, Geometry.HALF_PI), 4, Geometry.PI + QUARTER_PI, Geometry.HALF_PI); + checkSpherical(SphericalCoordinates.of(-4, MINUS_THREE_QUARTER_PI, Geometry.HALF_PI), 4, QUARTER_PI, Geometry.HALF_PI); + } + + @Test + public void testOf_NaNAndInfinite() { + // act/assert + checkSpherical(SphericalCoordinates.of(Double.NaN, Double.NaN, Double.NaN), + Double.NaN, Double.NaN, Double.NaN); + checkSpherical(SphericalCoordinates.of(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), + Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + checkSpherical(SphericalCoordinates.of(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY), + Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); + } + + @Test + public void testOfCartesian() { + // arrange + double sqrt3 = Math.sqrt(3); + + // act/assert + checkSpherical(SphericalCoordinates.ofCartesian(0, 0, 0), 0, 0, 0); + + checkSpherical(SphericalCoordinates.ofCartesian(0.1, 0, 0), 0.1, 0, Geometry.HALF_PI); + checkSpherical(SphericalCoordinates.ofCartesian(-0.1, 0, 0), 0.1, Geometry.PI, Geometry.HALF_PI); + + checkSpherical(SphericalCoordinates.ofCartesian(0, 0.1, 0), 0.1, Geometry.HALF_PI, Geometry.HALF_PI); + checkSpherical(SphericalCoordinates.ofCartesian(0, -0.1, 0), 0.1, Geometry.THREE_HALVES_PI, Geometry.HALF_PI); + + checkSpherical(SphericalCoordinates.ofCartesian(0, 0, 0.1), 0.1, 0, 0); + checkSpherical(SphericalCoordinates.ofCartesian(0, 0, -0.1), 0.1, 0, Geometry.PI); + + checkSpherical(SphericalCoordinates.ofCartesian(1, 1, 1), sqrt3, QUARTER_PI, Math.acos(1 / sqrt3)); + checkSpherical(SphericalCoordinates.ofCartesian(-1, -1, -1), sqrt3, 1.25 * Geometry.PI, Math.acos(-1 / sqrt3)); + } + + @Test + public void testToPoint() { + // arrange + double sqrt3 = Math.sqrt(3); + + // act/assert + checkPoint(SphericalCoordinates.of(0, 0, 0).toPoint(), 0, 0, 0); + + checkPoint(SphericalCoordinates.of(1, 0, Geometry.HALF_PI).toPoint(), 1, 0, 0); + checkPoint(SphericalCoordinates.of(1, Geometry.PI, Geometry.HALF_PI).toPoint(), -1, 0, 0); + + checkPoint(SphericalCoordinates.of(2, Geometry.HALF_PI, Geometry.HALF_PI).toPoint(), 0, 2, 0); + checkPoint(SphericalCoordinates.of(2, Geometry.MINUS_HALF_PI, Geometry.HALF_PI).toPoint(), 0, -2, 0); + + checkPoint(SphericalCoordinates.of(3, 0, 0).toPoint(), 0, 0, 3); + checkPoint(SphericalCoordinates.of(3, 0, Geometry.PI).toPoint(), 0, 0, -3); + + checkPoint(SphericalCoordinates.of(Math.sqrt(3), QUARTER_PI, Math.acos(1 / sqrt3)).toPoint(), 1, 1, 1); + checkPoint(SphericalCoordinates.of(Math.sqrt(3), MINUS_THREE_QUARTER_PI, Math.acos(-1 / sqrt3)).toPoint(), -1, -1, -1); + } + + @Test + public void testToVector() { + // arrange + double sqrt3 = Math.sqrt(3); + + // act/assert + checkVector(SphericalCoordinates.of(0, 0, 0).toVector(), 0, 0, 0); + + checkVector(SphericalCoordinates.of(1, 0, Geometry.HALF_PI).toVector(), 1, 0, 0); + checkVector(SphericalCoordinates.of(1, Geometry.PI, Geometry.HALF_PI).toVector(), -1, 0, 0); + + checkVector(SphericalCoordinates.of(2, Geometry.HALF_PI, Geometry.HALF_PI).toVector(), 0, 2, 0); + checkVector(SphericalCoordinates.of(2, Geometry.MINUS_HALF_PI, Geometry.HALF_PI).toVector(), 0, -2, 0); + + checkVector(SphericalCoordinates.of(3, 0, 0).toVector(), 0, 0, 3); + checkVector(SphericalCoordinates.of(3, 0, Geometry.PI).toVector(), 0, 0, -3); + + checkVector(SphericalCoordinates.of(sqrt3, QUARTER_PI, Math.acos(1 / sqrt3)).toVector(), 1, 1, 1); + checkVector(SphericalCoordinates.of(sqrt3, MINUS_THREE_QUARTER_PI, Math.acos(-1 / sqrt3)).toVector(), -1, -1, -1); + } + + @Test + public void testToCartesian_static() { + // arrange + double sqrt3 = Math.sqrt(3); + DoubleFunction3N<Point3D> factory = Point3D.FACTORY; + + // act/assert + checkPoint(SphericalCoordinates.toCartesian(0, 0, 0, factory), 0, 0, 0); + + checkPoint(SphericalCoordinates.toCartesian(1, 0, Geometry.HALF_PI, factory), 1, 0, 0); + checkPoint(SphericalCoordinates.toCartesian(1, Geometry.PI, Geometry.HALF_PI, factory), -1, 0, 0); + + checkPoint(SphericalCoordinates.toCartesian(2, Geometry.HALF_PI, Geometry.HALF_PI, factory), 0, 2, 0); + checkPoint(SphericalCoordinates.toCartesian(2, Geometry.MINUS_HALF_PI, Geometry.HALF_PI, factory), 0, -2, 0); + + checkPoint(SphericalCoordinates.toCartesian(3, 0, 0, factory), 0, 0, 3); + checkPoint(SphericalCoordinates.toCartesian(3, 0, Geometry.PI, factory), 0, 0, -3); + + checkPoint(SphericalCoordinates.toCartesian(Math.sqrt(3), QUARTER_PI, Math.acos(1 / sqrt3), factory), 1, 1, 1); + checkPoint(SphericalCoordinates.toCartesian(Math.sqrt(3), MINUS_THREE_QUARTER_PI, Math.acos(-1 / sqrt3), factory), -1, -1, -1); + } + + @Test + public void testGetDimension() { + // arrange + SphericalCoordinates s = SphericalCoordinates.of(0, 0, 0); + + // act/assert + Assert.assertEquals(3, s.getDimension()); + } + + @Test + public void testNaN() { + // act/assert + Assert.assertTrue(SphericalCoordinates.of(0, 0, Double.NaN).isNaN()); + Assert.assertTrue(SphericalCoordinates.of(0, Double.NaN, 0).isNaN()); + Assert.assertTrue(SphericalCoordinates.of(Double.NaN, 0, 0).isNaN()); + + Assert.assertFalse(SphericalCoordinates.of(1, 1, 1).isNaN()); + Assert.assertFalse(SphericalCoordinates.of(1, 1, Double.NEGATIVE_INFINITY).isNaN()); + Assert.assertFalse(SphericalCoordinates.of(1, Double.POSITIVE_INFINITY, 1).isNaN()); + Assert.assertFalse(SphericalCoordinates.of(Double.NEGATIVE_INFINITY, 1, 1).isNaN()); + } + + @Test + public void testInfinite() { + // act/assert + Assert.assertTrue(SphericalCoordinates.of(0, 0, Double.NEGATIVE_INFINITY).isInfinite()); + Assert.assertTrue(SphericalCoordinates.of(0, Double.NEGATIVE_INFINITY, 0).isInfinite()); + Assert.assertTrue(SphericalCoordinates.of(Double.NEGATIVE_INFINITY, 0, 0).isInfinite()); + Assert.assertTrue(SphericalCoordinates.of(0, 0, Double.POSITIVE_INFINITY).isInfinite()); + Assert.assertTrue(SphericalCoordinates.of(0, Double.POSITIVE_INFINITY, 0).isInfinite()); + Assert.assertTrue(SphericalCoordinates.of(Double.POSITIVE_INFINITY, 0, 0).isInfinite()); + + Assert.assertFalse(SphericalCoordinates.of(1, 1, 1).isInfinite()); + Assert.assertFalse(SphericalCoordinates.of(0, 0, Double.NaN).isInfinite()); + Assert.assertFalse(SphericalCoordinates.of(0, Double.NEGATIVE_INFINITY, Double.NaN).isInfinite()); + Assert.assertFalse(SphericalCoordinates.of(Double.NaN, 0, Double.NEGATIVE_INFINITY).isInfinite()); + Assert.assertFalse(SphericalCoordinates.of(Double.POSITIVE_INFINITY, Double.NaN, 0).isInfinite()); + Assert.assertFalse(SphericalCoordinates.of(0, Double.NaN, Double.POSITIVE_INFINITY).isInfinite()); + } + + @Test + public void testHashCode() { + // arrange + SphericalCoordinates a = SphericalCoordinates.of(1, 2, 3); + SphericalCoordinates b = SphericalCoordinates.of(10, 2, 3); + SphericalCoordinates c = SphericalCoordinates.of(1, 20, 3); + SphericalCoordinates d = SphericalCoordinates.of(1, 2, 30); + + SphericalCoordinates e = SphericalCoordinates.of(1, 2, 3); + + // 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 + SphericalCoordinates a = SphericalCoordinates.of(1, 2, Double.NaN); + SphericalCoordinates b = SphericalCoordinates.of(1, Double.NaN, 3); + SphericalCoordinates c = SphericalCoordinates.of(Double.NaN, 2, 3); + + // act/assert + Assert.assertEquals(a.hashCode(), b.hashCode()); + Assert.assertEquals(b.hashCode(), c.hashCode()); + } + + @Test + public void testEquals() { + // arrange + SphericalCoordinates a = SphericalCoordinates.of(1, 2, 3); + SphericalCoordinates b = SphericalCoordinates.of(10, 2, 3); + SphericalCoordinates c = SphericalCoordinates.of(1, 20, 3); + SphericalCoordinates d = SphericalCoordinates.of(1, 2, 30); + + SphericalCoordinates e = SphericalCoordinates.of(1, 2, 3); + + // 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 + SphericalCoordinates a = SphericalCoordinates.of(1, 2, Double.NaN); + SphericalCoordinates b = SphericalCoordinates.of(1, Double.NaN, 3); + SphericalCoordinates c = SphericalCoordinates.of(Double.NaN, 2, 3); + + // act/assert + Assert.assertTrue(a.equals(b)); + Assert.assertTrue(b.equals(c)); + } + + @Test + public void testToString() { + // arrange + SphericalCoordinates sph = SphericalCoordinates.of(1, 2, 3); + Pattern pattern = Pattern.compile("\\(1.{0,2}, 2.{0,2}, 3.{0,2}\\)"); + + // act + String str = sph.toString();; + + // assert + Assert.assertTrue("Expected string " + str + " to match regex " + pattern, + pattern.matcher(str).matches()); + } + + @Test + public void testParse() { + // act/assert + checkSpherical(SphericalCoordinates.parse("(1, 2, 3)"), 1, 2, 3); + checkSpherical(SphericalCoordinates.parse("( -2.0 , 1 , -5e-1)"), 2, 1 + Geometry.PI, Geometry.PI - 0.5); + checkSpherical(SphericalCoordinates.parse("(NaN,Infinity,-Infinity)"), Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY); + } + + @Test(expected = IllegalArgumentException.class) + public void testParse_failure() { + // act/assert + SphericalCoordinates.parse("abc"); + } + + @Test + public void testNormalizeAzimuth() { + // act/assert + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(0), 0.0, EPS); + + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(Geometry.HALF_PI), Geometry.HALF_PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(Geometry.PI), Geometry.PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(Geometry.THREE_HALVES_PI), Geometry.THREE_HALVES_PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(Geometry.TWO_PI), 0.0, EPS); + + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(Geometry.MINUS_HALF_PI), Geometry.THREE_HALVES_PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(-Geometry.PI), Geometry.PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(-Geometry.PI - Geometry.HALF_PI), Geometry.HALF_PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(-Geometry.TWO_PI), 0.0, EPS); + } + + @Test + public void testNormalizeAzimuth_NaNAndInfinite() { + // act/assert + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(Double.NaN), Double.NaN, EPS); + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(Double.NEGATIVE_INFINITY), Double.NEGATIVE_INFINITY, EPS); + Assert.assertEquals(SphericalCoordinates.normalizeAzimuth(Double.POSITIVE_INFINITY), Double.POSITIVE_INFINITY, EPS); + } + + @Test + public void testNormalizePolar() { + // act/assert + Assert.assertEquals(SphericalCoordinates.normalizePolar(0), 0.0, EPS); + + Assert.assertEquals(SphericalCoordinates.normalizePolar(Geometry.HALF_PI), Geometry.HALF_PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizePolar(Geometry.PI), Geometry.PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizePolar(Geometry.PI + Geometry.HALF_PI), Geometry.HALF_PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizePolar(Geometry.TWO_PI), 0.0, EPS); + + Assert.assertEquals(SphericalCoordinates.normalizePolar(Geometry.MINUS_HALF_PI), Geometry.HALF_PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizePolar(-Geometry.PI), Geometry.PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizePolar(-Geometry.PI - Geometry.HALF_PI), Geometry.HALF_PI, EPS); + Assert.assertEquals(SphericalCoordinates.normalizePolar(-Geometry.TWO_PI), 0.0, EPS); + } + + @Test + public void testNormalizePolar_NaNAndInfinite() { + // act/assert + Assert.assertEquals(SphericalCoordinates.normalizePolar(Double.NaN), Double.NaN, EPS); + Assert.assertEquals(SphericalCoordinates.normalizePolar(Double.NEGATIVE_INFINITY), Double.NEGATIVE_INFINITY, EPS); + Assert.assertEquals(SphericalCoordinates.normalizePolar(Double.POSITIVE_INFINITY), Double.POSITIVE_INFINITY, EPS); + } + + private void checkSpherical(SphericalCoordinates c, double radius, double azimuth, double polar) { + Assert.assertEquals(radius, c.getRadius(), EPS); + Assert.assertEquals(azimuth, c.getAzimuth(), EPS); + Assert.assertEquals(polar, c.getPolar(), EPS); + } + + private void checkPoint(Point3D p, double x, double y, double z) { + Assert.assertEquals(x, p.getX(), EPS); + Assert.assertEquals(y, p.getY(), EPS); + Assert.assertEquals(z, p.getZ(), EPS); + } + + private void checkVector(Vector3D v, double x, double y, double z) { + Assert.assertEquals(x, v.getX(), EPS); + Assert.assertEquals(y, v.getY(), EPS); + Assert.assertEquals(z, v.getZ(), EPS); + } +} diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java index c4017bd..f6a1606 100644 --- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java @@ -28,7 +28,7 @@ public class Vector3DTest { - private static final double EPS = Math.ulp(1d); + private static final double EPS = 1e-15; @Test public void testConstants() { @@ -687,6 +687,27 @@ public void testOf_arrayArg_invalidDimensions() { Vector3D.of(new double[] { 0.0, 0.0 }); } + @Test + public void testOfSpherical() { + // arrange + double sqrt3 = Math.sqrt(3); + + // act/assert + checkVector(Vector3D.ofSpherical(0, 0, 0), 0, 0, 0); + + checkVector(Vector3D.ofSpherical(1, 0, Geometry.HALF_PI), 1, 0, 0); + checkVector(Vector3D.ofSpherical(1, Geometry.PI, Geometry.HALF_PI), -1, 0, 0); + + checkVector(Vector3D.ofSpherical(2, Geometry.HALF_PI, Geometry.HALF_PI), 0, 2, 0); + checkVector(Vector3D.ofSpherical(2, Geometry.MINUS_HALF_PI, Geometry.HALF_PI), 0, -2, 0); + + checkVector(Vector3D.ofSpherical(3, 0, 0), 0, 0, 3); + checkVector(Vector3D.ofSpherical(3, 0, Geometry.PI), 0, 0, -3); + + checkVector(Vector3D.ofSpherical(sqrt3, 0.25 * Geometry.PI, Math.acos(1 / sqrt3)), 1, 1, 1); + checkVector(Vector3D.ofSpherical(sqrt3, -0.75 * Geometry.PI, Math.acos(-1 / sqrt3)), -1, -1, -1); + } + @Test public void testLinearCombination1() { // arrange @@ -738,41 +759,6 @@ public void testLinearCombination4() { checkVector(Vector3D.linearCombination(-3, p1, 2, p2, -4, p3, 5, p4), -64, -78, -2); } - @Test - public void testConstructors() { - double r = Math.sqrt(2) /2; - checkVector(Vector3D.linearCombination(2, Vector3D.fromSpherical(Math.PI / 3, -Math.PI / 4)), - r, r * Math.sqrt(3), -2 * r); - checkVector(Vector3D.linearCombination(2, Vector3D.PLUS_X, - -3, Vector3D.MINUS_Z), - 2, 0, 3); - checkVector(Vector3D.linearCombination(2, Vector3D.PLUS_X, - 5, Vector3D.PLUS_Y, - -3, Vector3D.MINUS_Z), - 2, 5, 3); - checkVector(Vector3D.linearCombination(2, Vector3D.PLUS_X, - 5, Vector3D.PLUS_Y, - 5, Vector3D.MINUS_Y, - -3, Vector3D.MINUS_Z), - 2, 0, 3); - checkVector(Vector3D.of(new double[] { 2, 5, -3 }), - 2, 5, -3); - } - - @Test - public void testAngular() { - Assert.assertEquals(0, Vector3D.PLUS_X.getAlpha(), 1.0e-10); - Assert.assertEquals(0, Vector3D.PLUS_X.getDelta(), 1.0e-10); - Assert.assertEquals(Math.PI / 2, Vector3D.PLUS_Y.getAlpha(), 1.0e-10); - Assert.assertEquals(0, Vector3D.PLUS_Y.getDelta(), 1.0e-10); - Assert.assertEquals(0, Vector3D.PLUS_Z.getAlpha(), 1.0e-10); - Assert.assertEquals(Math.PI / 2, Vector3D.PLUS_Z.getDelta(), 1.0e-10); - - Vector3D u = Vector3D.of(-1, 1, -1); - Assert.assertEquals(3 * Math.PI /4, u.getAlpha(), 1.0e-10); - Assert.assertEquals(-1.0 / Math.sqrt(3), Math.sin(u.getDelta()), 1.0e-10); - } - private void checkVector(Vector3D v, double x, double y, double z) { Assert.assertEquals(x, v.getX(), EPS); Assert.assertEquals(y, v.getY(), EPS); 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 e3d5127..5852616 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 @@ -2,10 +2,10 @@ import java.util.regex.Pattern; +import org.apache.commons.geometry.core.Geometry; import org.junit.Assert; import org.junit.Test; - public class Cartesian2DTest { private static final double TEST_TOLERANCE = 1e-15; @@ -70,6 +70,33 @@ public void testInfinite() { 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.THREE_HALVES_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, 1.75 * Geometry.PI); + checkPolar(new StubCartesian2D(-sqrt2, -sqrt2).toPolar(), 2, 1.25 * 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()); + } + @Test public void testToString() { // arrange @@ -84,6 +111,11 @@ public void testToString() { pattern.matcher(str).matches()); } + 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 e6cf422..69c3aa3 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 @@ import java.util.regex.Pattern; +import org.apache.commons.geometry.core.Geometry; import org.apache.commons.numbers.core.Precision; import org.junit.Assert; import org.junit.Test; @@ -218,6 +219,28 @@ public void testOf_arrayArg_invalidDimensions() { Point2D.of(new double[] {0.0 }); } + @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 testVectorCombination1() { // arrange @@ -275,7 +298,11 @@ private void checkVector(Vector2D v, double x, double y) { } 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..524e7e0 --- /dev/null +++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinatesTest.java @@ -0,0 +1,362 @@ +/* + * 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.internal.DoubleFunction2N; +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.THREE_HALVES_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.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, Geometry.THREE_HALVES_PI); + checkAzimuthWrapAround(2, Geometry.TWO_PI - delta); + } + + 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.THREE_HALVES_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, 1.25 * Geometry.PI); + + checkPolar(PolarCoordinates.ofCartesian(0, -1), 1, 1.5 * Geometry.PI); + checkPolar(PolarCoordinates.ofCartesian(1, -1), sqrt2, 1.75 * 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_static() { + // arrange + DoubleFunction2N<Point2D> factory = Point2D.FACTORY; + 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 + DoubleFunction2N<Point2D> factory = Point2D.FACTORY; + + // 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("\\(1.{0,2}, 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("(1, 2)"), 1, 2); + checkPolar(PolarCoordinates.parse("( -1 , 0.5 )"), 1, 0.5 + Geometry.PI); + checkPolar(PolarCoordinates.parse("(NaN,-Infinity)"), Double.NaN, Double.NEGATIVE_INFINITY); + } + + @Test(expected = IllegalArgumentException.class) + public void testParse_failure() { + // act/assert + PolarCoordinates.parse("abc"); + } + + @Test + public void testNormalizeAzimuth() { + // act/assert + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(0), 0.0, EPS); + + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(Geometry.HALF_PI), Geometry.HALF_PI, EPS); + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(Geometry.PI), Geometry.PI, EPS); + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(Geometry.THREE_HALVES_PI), Geometry.THREE_HALVES_PI, EPS); + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(Geometry.TWO_PI), 0.0, EPS); + + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(Geometry.MINUS_HALF_PI), Geometry.THREE_HALVES_PI, EPS); + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(-Geometry.PI), Geometry.PI, EPS); + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(-Geometry.PI - Geometry.HALF_PI), Geometry.HALF_PI, EPS); + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(-Geometry.TWO_PI), 0.0, EPS); + } + + @Test + public void testNormalizeAzimuth_NaNAndInfinite() { + // act/assert + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(Double.NaN), Double.NaN, EPS); + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(Double.NEGATIVE_INFINITY), Double.NEGATIVE_INFINITY, EPS); + Assert.assertEquals(PolarCoordinates.normalizeAzimuth(Double.POSITIVE_INFINITY), Double.POSITIVE_INFINITY, EPS); + } + + 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 83d56a1..1ef8d3b 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 @@ -482,6 +482,29 @@ public void testOf_arrayArg_invalidDimensions() { // act/assert Vector2D.of(new double[] {0.0 }); } + + @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 testLinearCombination1() { // arrange @@ -534,8 +557,12 @@ public void testLinearCombination4() { } 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) { diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/SphericalCoordinates.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/SphericalCoordinates.java deleted file mode 100644 index d9d959f..0000000 --- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/SphericalCoordinates.java +++ /dev/null @@ -1,394 +0,0 @@ -/* - * 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.spherical; - - -import java.io.Serializable; - -import org.apache.commons.geometry.euclidean.threed.Vector3D; - -/** This class provides conversions related to <a - * href="http://mathworld.wolfram.com/SphericalCoordinates.html">spherical coordinates</a>. - * <p> - * The conventions used here are the mathematical ones, i.e. spherical coordinates are - * related to Cartesian coordinates as follows: - * </p> - * <ul> - * <li>x = r cos(θ) sin(Φ)</li> - * <li>y = r sin(θ) sin(Φ)</li> - * <li>z = r cos(Φ)</li> - * </ul> - * <ul> - * <li>r = √(x<sup>2</sup>+y<sup>2</sup>+z<sup>2</sup>)</li> - * <li>θ = atan2(y, x)</li> - * <li>Φ = acos(z/r)</li> - * </ul> - * <p> - * r is the radius, θ is the azimuthal angle in the x-y plane and Φ is the polar - * (co-latitude) angle. These conventions are <em>different</em> from the conventions used - * in physics (and in particular in spherical harmonics) where the meanings of θ and - * Φ are reversed. - * </p> - * <p> - * This class provides conversion of coordinates and also of gradient and Hessian - * between spherical and Cartesian coordinates. - * </p> - */ -public class SphericalCoordinates implements Serializable { - - /** Serializable UID. */ - private static final long serialVersionUID = 20130206L; - - /** Cartesian coordinates. */ - private final Vector3D v; - - /** Radius. */ - private final double r; - - /** Azimuthal angle in the x-y plane θ. */ - private final double theta; - - /** Polar angle (co-latitude) Φ. */ - private final double phi; - - /** Jacobian of (r, θ Φ). */ - private double[][] jacobian; - - /** Hessian of radius. */ - private double[][] rHessian; - - /** Hessian of azimuthal angle in the x-y plane θ. */ - private double[][] thetaHessian; - - /** Hessian of polar (co-latitude) angle Φ. */ - private double[][] phiHessian; - - /** Build a spherical coordinates transformer from Cartesian coordinates. - * @param v Cartesian coordinates - */ - public SphericalCoordinates(final Vector3D v) { - - // Cartesian coordinates - this.v = v; - - // remaining spherical coordinates - this.r = v.getNorm(); - this.theta = v.getAlpha(); - this.phi = Math.acos(v.getZ() / r); - - } - - /** Build a spherical coordinates transformer from spherical coordinates. - * @param r radius - * @param theta azimuthal angle in x-y plane - * @param phi polar (co-latitude) angle - */ - public SphericalCoordinates(final double r, final double theta, final double phi) { - - final double cosTheta = Math.cos(theta); - final double sinTheta = Math.sin(theta); - final double cosPhi = Math.cos(phi); - final double sinPhi = Math.sin(phi); - - // spherical coordinates - this.r = r; - this.theta = theta; - this.phi = phi; - - // Cartesian coordinates - this.v = Vector3D.of(r * cosTheta * sinPhi, - r * sinTheta * sinPhi, - r * cosPhi); - - } - - /** Get the Cartesian coordinates. - * @return Cartesian coordinates - */ - public Vector3D getCartesian() { - return v; - } - - /** Get the radius. - * @return radius r - * @see #getTheta() - * @see #getPhi() - */ - public double getR() { - return r; - } - - /** Get the azimuthal angle in x-y plane. - * @return azimuthal angle in x-y plane θ - * @see #getR() - * @see #getPhi() - */ - public double getTheta() { - return theta; - } - - /** Get the polar (co-latitude) angle. - * @return polar (co-latitude) angle Φ - * @see #getR() - * @see #getTheta() - */ - public double getPhi() { - return phi; - } - - /** Convert a gradient with respect to spherical coordinates into a gradient - * with respect to Cartesian coordinates. - * @param sGradient gradient with respect to spherical coordinates - * {df/dr, df/dθ, df/dΦ} - * @return gradient with respect to Cartesian coordinates - * {df/dx, df/dy, df/dz} - */ - public double[] toCartesianGradient(final double[] sGradient) { - - // lazy evaluation of Jacobian - computeJacobian(); - - // compose derivatives as gradient^T . J - // the expressions have been simplified since we know jacobian[1][2] = dTheta/dZ = 0 - return new double[] { - sGradient[0] * jacobian[0][0] + sGradient[1] * jacobian[1][0] + sGradient[2] * jacobian[2][0], - sGradient[0] * jacobian[0][1] + sGradient[1] * jacobian[1][1] + sGradient[2] * jacobian[2][1], - sGradient[0] * jacobian[0][2] + sGradient[2] * jacobian[2][2] - }; - - } - - /** Convert a Hessian with respect to spherical coordinates into a Hessian - * with respect to Cartesian coordinates. - * <p> - * As Hessian are always symmetric, we use only the lower left part of the provided - * spherical Hessian, so the upper part may not be initialized. However, we still - * do fill up the complete array we create, with guaranteed symmetry. - * </p> - * @param sHessian Hessian with respect to spherical coordinates - * {{d<sup>2</sup>f/dr<sup>2</sup>, d<sup>2</sup>f/drdθ, d<sup>2</sup>f/drdΦ}, - * {d<sup>2</sup>f/drdθ, d<sup>2</sup>f/dθ<sup>2</sup>, d<sup>2</sup>f/dθdΦ}, - * {d<sup>2</sup>f/drdΦ, d<sup>2</sup>f/dθdΦ, d<sup>2</sup>f/dΦ<sup>2</sup>} - * @param sGradient gradient with respect to spherical coordinates - * {df/dr, df/dθ, df/dΦ} - * @return Hessian with respect to Cartesian coordinates - * {{d<sup>2</sup>f/dx<sup>2</sup>, d<sup>2</sup>f/dxdy, d<sup>2</sup>f/dxdz}, - * {d<sup>2</sup>f/dxdy, d<sup>2</sup>f/dy<sup>2</sup>, d<sup>2</sup>f/dydz}, - * {d<sup>2</sup>f/dxdz, d<sup>2</sup>f/dydz, d<sup>2</sup>f/dz<sup>2</sup>}} - */ - public double[][] toCartesianHessian(final double[][] sHessian, final double[] sGradient) { - - computeJacobian(); - computeHessians(); - - // compose derivative as J^T . H_f . J + df/dr H_r + df/dtheta H_theta + df/dphi H_phi - // the expressions have been simplified since we know jacobian[1][2] = dTheta/dZ = 0 - // and H_theta is only a 2x2 matrix as it does not depend on z - final double[][] hj = new double[3][3]; - final double[][] cHessian = new double[3][3]; - - // compute H_f . J - // beware we use ONLY the lower-left part of sHessian - hj[0][0] = sHessian[0][0] * jacobian[0][0] + sHessian[1][0] * jacobian[1][0] + sHessian[2][0] * jacobian[2][0]; - hj[0][1] = sHessian[0][0] * jacobian[0][1] + sHessian[1][0] * jacobian[1][1] + sHessian[2][0] * jacobian[2][1]; - hj[0][2] = sHessian[0][0] * jacobian[0][2] + sHessian[2][0] * jacobian[2][2]; - hj[1][0] = sHessian[1][0] * jacobian[0][0] + sHessian[1][1] * jacobian[1][0] + sHessian[2][1] * jacobian[2][0]; - hj[1][1] = sHessian[1][0] * jacobian[0][1] + sHessian[1][1] * jacobian[1][1] + sHessian[2][1] * jacobian[2][1]; - // don't compute hj[1][2] as it is not used below - hj[2][0] = sHessian[2][0] * jacobian[0][0] + sHessian[2][1] * jacobian[1][0] + sHessian[2][2] * jacobian[2][0]; - hj[2][1] = sHessian[2][0] * jacobian[0][1] + sHessian[2][1] * jacobian[1][1] + sHessian[2][2] * jacobian[2][1]; - hj[2][2] = sHessian[2][0] * jacobian[0][2] + sHessian[2][2] * jacobian[2][2]; - - // compute lower-left part of J^T . H_f . J - cHessian[0][0] = jacobian[0][0] * hj[0][0] + jacobian[1][0] * hj[1][0] + jacobian[2][0] * hj[2][0]; - cHessian[1][0] = jacobian[0][1] * hj[0][0] + jacobian[1][1] * hj[1][0] + jacobian[2][1] * hj[2][0]; - cHessian[2][0] = jacobian[0][2] * hj[0][0] + jacobian[2][2] * hj[2][0]; - cHessian[1][1] = jacobian[0][1] * hj[0][1] + jacobian[1][1] * hj[1][1] + jacobian[2][1] * hj[2][1]; - cHessian[2][1] = jacobian[0][2] * hj[0][1] + jacobian[2][2] * hj[2][1]; - cHessian[2][2] = jacobian[0][2] * hj[0][2] + jacobian[2][2] * hj[2][2]; - - // add gradient contribution - cHessian[0][0] += sGradient[0] * rHessian[0][0] + sGradient[1] * thetaHessian[0][0] + sGradient[2] * phiHessian[0][0]; - cHessian[1][0] += sGradient[0] * rHessian[1][0] + sGradient[1] * thetaHessian[1][0] + sGradient[2] * phiHessian[1][0]; - cHessian[2][0] += sGradient[0] * rHessian[2][0] + sGradient[2] * phiHessian[2][0]; - cHessian[1][1] += sGradient[0] * rHessian[1][1] + sGradient[1] * thetaHessian[1][1] + sGradient[2] * phiHessian[1][1]; - cHessian[2][1] += sGradient[0] * rHessian[2][1] + sGradient[2] * phiHessian[2][1]; - cHessian[2][2] += sGradient[0] * rHessian[2][2] + sGradient[2] * phiHessian[2][2]; - - // ensure symmetry - cHessian[0][1] = cHessian[1][0]; - cHessian[0][2] = cHessian[2][0]; - cHessian[1][2] = cHessian[2][1]; - - return cHessian; - - } - - /** Lazy evaluation of (r, θ, φ) Jacobian. - */ - private void computeJacobian() { - if (jacobian == null) { - - // intermediate variables - final double x = v.getX(); - final double y = v.getY(); - final double z = v.getZ(); - final double rho2 = x * x + y * y; - final double rho = Math.sqrt(rho2); - final double r2 = rho2 + z * z; - - jacobian = new double[3][3]; - - // row representing the gradient of r - jacobian[0][0] = x / r; - jacobian[0][1] = y / r; - jacobian[0][2] = z / r; - - // row representing the gradient of theta - jacobian[1][0] = -y / rho2; - jacobian[1][1] = x / rho2; - // jacobian[1][2] is already set to 0 at allocation time - - // row representing the gradient of phi - jacobian[2][0] = x * z / (rho * r2); - jacobian[2][1] = y * z / (rho * r2); - jacobian[2][2] = -rho / r2; - - } - } - - /** Lazy evaluation of Hessians. - */ - private void computeHessians() { - - if (rHessian == null) { - - // intermediate variables - final double x = v.getX(); - final double y = v.getY(); - final double z = v.getZ(); - final double x2 = x * x; - final double y2 = y * y; - final double z2 = z * z; - final double rho2 = x2 + y2; - final double rho = Math.sqrt(rho2); - final double r2 = rho2 + z2; - final double xOr = x / r; - final double yOr = y / r; - final double zOr = z / r; - final double xOrho2 = x / rho2; - final double yOrho2 = y / rho2; - final double xOr3 = xOr / r2; - final double yOr3 = yOr / r2; - final double zOr3 = zOr / r2; - - // lower-left part of Hessian of r - rHessian = new double[3][3]; - rHessian[0][0] = y * yOr3 + z * zOr3; - rHessian[1][0] = -x * yOr3; - rHessian[2][0] = -z * xOr3; - rHessian[1][1] = x * xOr3 + z * zOr3; - rHessian[2][1] = -y * zOr3; - rHessian[2][2] = x * xOr3 + y * yOr3; - - // upper-right part is symmetric - rHessian[0][1] = rHessian[1][0]; - rHessian[0][2] = rHessian[2][0]; - rHessian[1][2] = rHessian[2][1]; - - // lower-left part of Hessian of azimuthal angle theta - thetaHessian = new double[2][2]; - thetaHessian[0][0] = 2 * xOrho2 * yOrho2; - thetaHessian[1][0] = yOrho2 * yOrho2 - xOrho2 * xOrho2; - thetaHessian[1][1] = -2 * xOrho2 * yOrho2; - - // upper-right part is symmetric - thetaHessian[0][1] = thetaHessian[1][0]; - - // lower-left part of Hessian of polar (co-latitude) angle phi - final double rhor2 = rho * r2; - final double rho2r2 = rho * rhor2; - final double rhor4 = rhor2 * r2; - final double rho3r4 = rhor4 * rho2; - final double r2P2rho2 = 3 * rho2 + z2; - phiHessian = new double[3][3]; - phiHessian[0][0] = z * (rho2r2 - x2 * r2P2rho2) / rho3r4; - phiHessian[1][0] = -x * y * z * r2P2rho2 / rho3r4; - phiHessian[2][0] = x * (rho2 - z2) / rhor4; - phiHessian[1][1] = z * (rho2r2 - y2 * r2P2rho2) / rho3r4; - phiHessian[2][1] = y * (rho2 - z2) / rhor4; - phiHessian[2][2] = 2 * rho * zOr3 / r; - - // upper-right part is symmetric - phiHessian[0][1] = phiHessian[1][0]; - phiHessian[0][2] = phiHessian[2][0]; - phiHessian[1][2] = phiHessian[2][1]; - - } - - } - - /** - * Replace the instance with a data transfer object for serialization. - * @return data transfer object that will be serialized - */ - private Object writeReplace() { - return new DataTransferObject(v.getX(), v.getY(), v.getZ()); - } - - /** Internal class used only for serialization. */ - private static class DataTransferObject implements Serializable { - - /** Serializable UID. */ - private static final long serialVersionUID = 20130206L; - - /** Abscissa. - * @serial - */ - private final double x; - - /** Ordinate. - * @serial - */ - private final double y; - - /** Height. - * @serial - */ - private final double z; - - /** Simple constructor. - * @param x abscissa - * @param y ordinate - * @param z height - */ - DataTransferObject(final double x, final double y, final double z) { - this.x = x; - this.y = y; - this.z = z; - } - - /** Replace the deserialized data transfer object with a {@link SphericalCoordinates}. - * @return replacement {@link SphericalCoordinates} - */ - private Object readResolve() { - return new SphericalCoordinates(Vector3D.of(x, y, z)); - } - - } - -} diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/ArcsSet.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/ArcsSet.java index 813dae9..311950f 100644 --- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/ArcsSet.java +++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/ArcsSet.java @@ -435,7 +435,7 @@ private boolean isDirect(final BSPTree<S1Point> node) { * @return limit angle */ private double getAngle(final BSPTree<S1Point> node) { - return ((LimitAngle) node.getCut().getHyperplane()).getLocation().getAlpha(); + return ((LimitAngle) node.getCut().getHyperplane()).getLocation().getAzimuth(); } /** {@inheritDoc} */ @@ -475,7 +475,7 @@ protected void computeGeometricalProperties() { public BoundaryProjection<S1Point> projectToBoundary(final S1Point point) { // get position of test point - final double alpha = point.getAlpha(); + final double alpha = point.getAzimuth(); boolean wrapFirst = false; double first = Double.NaN; diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/LimitAngle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/LimitAngle.java index bc80a21..605a649 100644 --- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/LimitAngle.java +++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/LimitAngle.java @@ -58,7 +58,7 @@ public LimitAngle copySelf() { /** {@inheritDoc} */ @Override public double getOffset(final S1Point point) { - final double delta = point.getAlpha() - location.getAlpha(); + final double delta = point.getAzimuth() - location.getAzimuth(); return direct ? delta : -delta; } diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/S1Point.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/S1Point.java index 21139d2..a9df1aa 100644 --- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/S1Point.java +++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/S1Point.java @@ -21,17 +21,17 @@ import org.apache.commons.geometry.core.Point; import org.apache.commons.geometry.core.internal.DoubleFunction1N; import org.apache.commons.geometry.core.internal.SimpleTupleFormat; +import org.apache.commons.geometry.euclidean.twod.PolarCoordinates; import org.apache.commons.geometry.euclidean.twod.Vector2D; -import org.apache.commons.numbers.angle.PlaneAngleRadians; /** This class represents a point on the 1-sphere. * <p>Instances of this class are guaranteed to be immutable.</p> */ public final class S1Point implements Point<S1Point>, Serializable { - // CHECKSTYLE: stop ConstantName - /** A vector with all coordinates set to NaN. */ - public static final S1Point NaN = new S1Point(Double.NaN, Vector2D.NaN); + // CHECKSTYLE: stop ConstantName + /** A point with all coordinates set to NaN. */ + public static final S1Point NaN = new S1Point(Double.NaN); // CHECKSTYLE: resume ConstantName /** Serializable UID. */ @@ -43,31 +43,30 @@ /** {@inheritDoc} */ @Override public S1Point apply(double n) { - return S1Point.of(n); + return new S1Point(n); } }; - /** Azimuthal angle in radians \( \alpha \). */ - private final double alpha; + /** Azimuthal angle in radians. */ + private final double azimuth; /** Corresponding 2D normalized vector. */ private final Vector2D vector; /** Build a point from its internal components. - * @param alpha azimuthal angle \( \alpha \) - * @param vector corresponding vector + * @param azimuth azimuthal angle */ - private S1Point(final double alpha, final Vector2D vector) { - this.alpha = alpha; - this.vector = vector; + private S1Point(final double azimuth) { + this.azimuth = PolarCoordinates.normalizeAzimuth(azimuth); + this.vector = Double.isFinite(azimuth) ? Vector2D.ofPolar(1.0, azimuth) : Vector2D.NaN; } - /** Get the azimuthal angle in radians \( \alpha \). - * @return azimuthal angle \( \alpha \) + /** Get the azimuthal angle in radians. + * @return azimuthal angle * @see S1Point#of(double) */ - public double getAlpha() { - return alpha; + public double getAzimuth() { + return azimuth; } /** Get the corresponding normalized vector in the 2D Euclidean space. @@ -86,13 +85,13 @@ public int getDimension() { /** {@inheritDoc} */ @Override public boolean isNaN() { - return Double.isNaN(alpha); + return Double.isNaN(azimuth); } /** {@inheritDoc} */ @Override public boolean isInfinite() { - return !isNaN() && Double.isInfinite(alpha); + return !isNaN() && Double.isInfinite(azimuth); } /** {@inheritDoc} */ @@ -131,7 +130,6 @@ public static double distance(S1Point p1, S1Point p2) { */ @Override public boolean equals(Object other) { - if (this == other) { return true; } @@ -142,11 +140,10 @@ public boolean equals(Object other) { return this.isNaN(); } - return alpha == rhs.alpha; + return azimuth == rhs.azimuth; } return false; - } /** @@ -161,25 +158,22 @@ public int hashCode() { if (isNaN()) { return 542; } - return 1759 * Double.hashCode(alpha); + return 1759 * Double.hashCode(azimuth); } /** {@inheritDoc} */ @Override public String toString() { - return SimpleTupleFormat.getDefault().format(getAlpha()); + return SimpleTupleFormat.getDefault().format(getAzimuth()); } /** Creates a new point instance from the given azimuthal coordinate value. - * @param alpha azimuthal angle in radians \( \alpha \) + * @param azimuth azimuthal angle in radians * @return point instance with the given azimuth coordinate value - * @see #getAlpha() + * @see #getAzimuth() */ - public static S1Point of(double alpha) { - double normalizedAlpha = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(alpha); - Vector2D vector = Vector2D.of(Math.cos(normalizedAlpha), Math.sin(normalizedAlpha)); - - return new S1Point(normalizedAlpha, vector); + public static S1Point of(double azimuth) { + return new S1Point(azimuth); } /** Parses the given string and returns a new point instance. The expected string diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/package-info.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/package-info.java deleted file mode 100644 index 020a968..0000000 --- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ -/** - * - * <p> - * Base package for spherical geometry components. - * </p> - */ -package org.apache.commons.geometry.spherical; diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Circle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Circle.java index 31cc5c9..e0aaba0 100644 --- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Circle.java +++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/Circle.java @@ -168,7 +168,7 @@ public double getPhase(final Vector3D direction) { */ @Override public S2Point toSpace(final S1Point point) { - return S2Point.of(getPointAt(point.getAlpha())); + return S2Point.of(getPointAt(point.getAzimuth())); } /** Get a circle point from its phase around the circle. diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/S2Point.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/S2Point.java index 3fc8795..1031b4b 100644 --- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/S2Point.java +++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/S2Point.java @@ -21,40 +21,34 @@ import org.apache.commons.geometry.core.Point; import org.apache.commons.geometry.core.internal.DoubleFunction2N; import org.apache.commons.geometry.core.internal.SimpleTupleFormat; +import org.apache.commons.geometry.euclidean.threed.SphericalCoordinates; import org.apache.commons.geometry.euclidean.threed.Vector3D; /** This class represents a point on the 2-sphere. - * <p> - * We use the mathematical convention to use the azimuthal angle \( \theta \) - * in the x-y plane as the first coordinate, and the polar angle \( \varphi \) - * as the second coordinate (see <a - * href="http://mathworld.wolfram.com/SphericalCoordinates.html">Spherical - * Coordinates</a> in MathWorld). - * </p> * <p>Instances of this class are guaranteed to be immutable.</p> */ public final class S2Point implements Point<S2Point>, Serializable { - /** +I (coordinates: \( \theta = 0, \varphi = \pi/2 \)). */ + /** +I (coordinates: ( azimuth = 0, polar = pi/2 )). */ public static final S2Point PLUS_I = new S2Point(0, 0.5 * Math.PI, Vector3D.PLUS_X); - /** +J (coordinates: \( \theta = \pi/2, \varphi = \pi/2 \))). */ + /** +J (coordinates: ( azimuth = pi/2, polar = pi/2 ))). */ public static final S2Point PLUS_J = new S2Point(0.5 * Math.PI, 0.5 * Math.PI, Vector3D.PLUS_Y); - /** +K (coordinates: \( \theta = any angle, \varphi = 0 \)). */ + /** +K (coordinates: ( azimuth = any angle, polar = 0 )). */ public static final S2Point PLUS_K = new S2Point(0, 0, Vector3D.PLUS_Z); - /** -I (coordinates: \( \theta = \pi, \varphi = \pi/2 \)). */ + /** -I (coordinates: ( azimuth = pi, polar = pi/2 )). */ public static final S2Point MINUS_I = new S2Point(Math.PI, 0.5 * Math.PI, Vector3D.MINUS_X); - /** -J (coordinates: \( \theta = 3\pi/2, \varphi = \pi/2 \)). */ + /** -J (coordinates: ( azimuth = 3pi/2, polar = pi/2 )). */ public static final S2Point MINUS_J = new S2Point(1.5 * Math.PI, 0.5 * Math.PI, Vector3D.MINUS_Y); - /** -K (coordinates: \( \theta = any angle, \varphi = \pi \)). */ + /** -K (coordinates: ( azimuth = any angle, polar = pi )). */ public static final S2Point MINUS_K = new S2Point(0, Math.PI, Vector3D.MINUS_Z); // CHECKSTYLE: stop ConstantName - /** A vector with all coordinates set to NaN. */ + /** A point with all coordinates set to NaN. */ public static final S2Point NaN = new S2Point(Double.NaN, Double.NaN, Vector3D.NaN); // CHECKSTYLE: resume ConstantName @@ -71,40 +65,40 @@ public S2Point apply(double n1, double n2) { } }; - /** Azimuthal angle \( \theta \) in the x-y plane. */ - private final double theta; + /** Azimuthal angle in the x-y plane. */ + private final double azimuth; - /** Polar angle \( \varphi \). */ - private final double phi; + /** Polar angle. */ + private final double polar; /** Corresponding 3D normalized vector. */ private final Vector3D vector; /** Build a point from its internal components. - * @param theta azimuthal angle \( \theta \) in the x-y plane - * @param phi polar angle \( \varphi \) - * @param vector corresponding vector + * @param azimuth azimuthal angle in the x-y plane + * @param polar polar angle + * @param vector corresponding vector; if null, the vector is computed */ - private S2Point(final double theta, final double phi, final Vector3D vector) { - this.theta = theta; - this.phi = phi; - this.vector = vector; + private S2Point(final double azimuth, final double polar, final Vector3D vector) { + this.azimuth = SphericalCoordinates.normalizeAzimuth(azimuth); + this.polar = SphericalCoordinates.normalizePolar(polar); + this.vector = (vector != null) ? vector : Vector3D.ofSpherical(1.0, azimuth, polar); } - /** Get the azimuthal angle \( \theta \) in the x-y plane. - * @return azimuthal angle \( \theta \) in the x-y plane + /** Get the azimuthal angle in the x-y plane in radians. + * @return azimuthal angle in the x-y plane * @see S2Point#of(double, double) */ - public double getTheta() { - return theta; + public double getAzimuth() { + return azimuth; } - /** Get the polar angle \( \varphi \). - * @return polar angle \( \varphi \) + /** Get the polar angle in radians. + * @return polar angle * @see S2Point#of(double, double) */ - public double getPhi() { - return phi; + public double getPolar() { + return polar; } /** Get the corresponding normalized vector in the 3D Euclidean space. @@ -123,20 +117,20 @@ public int getDimension() { /** {@inheritDoc} */ @Override public boolean isNaN() { - return Double.isNaN(theta) || Double.isNaN(phi); + return Double.isNaN(azimuth) || Double.isNaN(polar); } /** {@inheritDoc} */ @Override public boolean isInfinite() { - return !isNaN() && (Double.isInfinite(theta) || Double.isInfinite(phi)); + return !isNaN() && (Double.isInfinite(azimuth) || Double.isInfinite(polar)); } /** Get the opposite of the instance. * @return a new vector which is opposite to the instance */ public S2Point negate() { - return new S2Point(-theta, Math.PI - phi, vector.negate()); + return new S2Point(-azimuth, Math.PI - polar, vector.negate()); } /** {@inheritDoc} */ @@ -175,7 +169,6 @@ public static double distance(S2Point p1, S2Point p2) { */ @Override public boolean equals(Object other) { - if (this == other) { return true; } @@ -186,7 +179,7 @@ public boolean equals(Object other) { return this.isNaN(); } - return (theta == rhs.theta) && (phi == rhs.phi); + return (azimuth == rhs.azimuth) && (polar == rhs.polar); } return false; } @@ -203,57 +196,35 @@ public int hashCode() { if (isNaN()) { return 542; } - return 134 * (37 * Double.hashCode(theta) + Double.hashCode(phi)); + return 134 * (37 * Double.hashCode(azimuth) + Double.hashCode(polar)); } /** {@inheritDoc} */ @Override public String toString() { - return SimpleTupleFormat.getDefault().format(getTheta(), getPhi()); + return SimpleTupleFormat.getDefault().format(getAzimuth(), getPolar()); } /** Build a vector from its spherical coordinates - * @param theta azimuthal angle \( \theta \) in the x-y plane - * @param phi polar angle \( \varphi \) + * @param azimuth azimuthal angle in the x-y plane + * @param polar polar angle * @return point instance with the given coordinates - * @see #getTheta() - * @see #getPhi() - * @exception IllegalArgumentException if \( \varphi \) is not in the [\( 0; \pi \)] range + * @see #getAzimuth() + * @see #getPolar() */ - public static S2Point of(final double theta, final double phi) { - return new S2Point(theta, phi, vector(theta, phi)); + public static S2Point of(final double azimuth, final double polar) { + return new S2Point(azimuth, polar, null); } /** Build a point from its underlying 3D vector * @param vector 3D vector * @return point instance with the coordinates determined by the given 3D vector - * @exception IllegalArgumentException if vector norm is zero + * @exception IllegalStateException if vector norm is zero */ public static S2Point of(final Vector3D vector) { - return new S2Point(Math.atan2(vector.getY(), vector.getX()), - Vector3D.PLUS_Z.angle(vector), - vector.normalize()); - } - - /** Build the normalized vector corresponding to spherical coordinates. - * @param theta azimuthal angle \( \theta \) in the x-y plane - * @param phi polar angle \( \varphi \) - * @return normalized vector - * @exception IllegalArgumentException if \( \varphi \) is not in the [\( 0; \pi \)] range - */ - private static Vector3D vector(final double theta, final double phi) - throws IllegalArgumentException { - - if (phi < 0 || phi > Math.PI) { - throw new IllegalArgumentException(phi + " is out of [" + 0 + ", " + Math.PI + "] range"); - } - - final double cosTheta = Math.cos(theta); - final double sinTheta = Math.sin(theta); - final double cosPhi = Math.cos(phi); - final double sinPhi = Math.sin(phi); + SphericalCoordinates coords = vector.toSpherical(); - return Vector3D.of(cosTheta * sinPhi, sinTheta * sinPhi, cosPhi); + return new S2Point(coords.getAzimuth(), coords.getPolar(), vector.normalize()); } /** Parses the given string and returns a new point instance. The expected string diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalCoordinatesTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalCoordinatesTest.java deleted file mode 100644 index db3b76e..0000000 --- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalCoordinatesTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.spherical; - -import org.apache.commons.geometry.euclidean.threed.Vector3D; -import org.apache.commons.geometry.core.GeometryTestUtils; -import org.junit.Assert; -import org.junit.Test; - -public class SphericalCoordinatesTest { - - @Test - public void testCoordinatesStoC() { - double piO2 = 0.5 * Math.PI; - SphericalCoordinates sc1 = new SphericalCoordinates(2.0, 0, piO2); - Assert.assertEquals(0, sc1.getCartesian().distance(Vector3D.of(2, 0, 0)), 1.0e-10); - SphericalCoordinates sc2 = new SphericalCoordinates(2.0, piO2, piO2); - Assert.assertEquals(0, sc2.getCartesian().distance(Vector3D.of(0, 2, 0)), 1.0e-10); - SphericalCoordinates sc3 = new SphericalCoordinates(2.0, Math.PI, piO2); - Assert.assertEquals(0, sc3.getCartesian().distance(Vector3D.of(-2, 0, 0)), 1.0e-10); - SphericalCoordinates sc4 = new SphericalCoordinates(2.0, -piO2, piO2); - Assert.assertEquals(0, sc4.getCartesian().distance(Vector3D.of(0, -2, 0)), 1.0e-10); - SphericalCoordinates sc5 = new SphericalCoordinates(2.0, 1.23456, 0); - Assert.assertEquals(0, sc5.getCartesian().distance(Vector3D.of(0, 0, 2)), 1.0e-10); - SphericalCoordinates sc6 = new SphericalCoordinates(2.0, 6.54321, Math.PI); - Assert.assertEquals(0, sc6.getCartesian().distance(Vector3D.of(0, 0, -2)), 1.0e-10); - } - - @Test - public void testCoordinatesCtoS() { - double piO2 = 0.5 * Math.PI; - SphericalCoordinates sc1 = new SphericalCoordinates(Vector3D.of(2, 0, 0)); - Assert.assertEquals(2, sc1.getR(), 1.0e-10); - Assert.assertEquals(0, sc1.getTheta(), 1.0e-10); - Assert.assertEquals(piO2, sc1.getPhi(), 1.0e-10); - SphericalCoordinates sc2 = new SphericalCoordinates(Vector3D.of(0, 2, 0)); - Assert.assertEquals(2, sc2.getR(), 1.0e-10); - Assert.assertEquals(piO2, sc2.getTheta(), 1.0e-10); - Assert.assertEquals(piO2, sc2.getPhi(), 1.0e-10); - SphericalCoordinates sc3 = new SphericalCoordinates(Vector3D.of(-2, 0, 0)); - Assert.assertEquals(2, sc3.getR(), 1.0e-10); - Assert.assertEquals(Math.PI, sc3.getTheta(), 1.0e-10); - Assert.assertEquals(piO2, sc3.getPhi(), 1.0e-10); - SphericalCoordinates sc4 = new SphericalCoordinates(Vector3D.of(0, -2, 0)); - Assert.assertEquals(2, sc4.getR(), 1.0e-10); - Assert.assertEquals(-piO2, sc4.getTheta(), 1.0e-10); - Assert.assertEquals(piO2, sc4.getPhi(), 1.0e-10); - SphericalCoordinates sc5 = new SphericalCoordinates(Vector3D.of(0, 0, 2)); - Assert.assertEquals(2, sc5.getR(), 1.0e-10); - // don't check theta on poles, as it is singular - Assert.assertEquals(0, sc5.getPhi(), 1.0e-10); - SphericalCoordinates sc6 = new SphericalCoordinates(Vector3D.of(0, 0, -2)); - Assert.assertEquals(2, sc6.getR(), 1.0e-10); - // don't check theta on poles, as it is singular - Assert.assertEquals(Math.PI, sc6.getPhi(), 1.0e-10); - } - - @Test - public void testSerialization() { - SphericalCoordinates a = new SphericalCoordinates(3, 2, 1); - SphericalCoordinates b = (SphericalCoordinates) GeometryTestUtils.serializeAndRecover(a); - Assert.assertEquals(0, a.getCartesian().distance(b.getCartesian()), 1.0e-10); - Assert.assertEquals(a.getR(), b.getR(), 1.0e-10); - Assert.assertEquals(a.getTheta(), b.getTheta(), 1.0e-10); - Assert.assertEquals(a.getPhi(), b.getPhi(), 1.0e-10); - } - -} diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalTestUtils.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalTestUtils.java index a43bc3d..6baa3b1 100644 --- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalTestUtils.java +++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/SphericalTestUtils.java @@ -46,7 +46,7 @@ public static String dump(final ArcsSet arcsSet) { protected void formatHyperplane(final Hyperplane<S1Point> hyperplane) { final LimitAngle h = (LimitAngle) hyperplane; getFormatter().format("%22.15e %b %22.15e", - h.getLocation().getAlpha(), h.isDirect(), h.getTolerance()); + h.getLocation().getAzimuth(), h.isDirect(), h.getTolerance()); } }; diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/LimitAngleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/LimitAngleTest.java index f9a9b3b..629f2b6 100644 --- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/LimitAngleTest.java +++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/LimitAngleTest.java @@ -26,7 +26,7 @@ public void testReversedLimit() { for (int k = -2; k < 3; ++k) { LimitAngle l = new LimitAngle(S1Point.of(1.0 + k * Geometry.TWO_PI), false, 1.0e-10); - Assert.assertEquals(l.getLocation().getAlpha(), l.getReverse().getLocation().getAlpha(), 1.0e-10); + Assert.assertEquals(l.getLocation().getAzimuth(), l.getReverse().getLocation().getAzimuth(), 1.0e-10); Assert.assertEquals(l.getTolerance(), l.getReverse().getTolerance(), 1.0e-10); Assert.assertTrue(l.sameOrientationAs(l)); Assert.assertFalse(l.sameOrientationAs(l.getReverse())); diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/S1PointTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/S1PointTest.java index ba81963..f67fc50 100644 --- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/S1PointTest.java +++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/S1PointTest.java @@ -55,7 +55,7 @@ public void testEquals() { @Test public void testDistance() { S1Point a = S1Point.of(1.0); - S1Point b = S1Point.of(a.getAlpha() + 0.5 * Math.PI); + S1Point b = S1Point.of(a.getAzimuth() + 0.5 * Math.PI); Assert.assertEquals(0.5 * Math.PI, a.distance(b), 1.0e-10); } @@ -80,7 +80,7 @@ public void testParse_failure() { } private void checkPoint(S1Point p, double alpha) { - Assert.assertEquals(alpha, p.getAlpha(), EPS); + Assert.assertEquals(alpha, p.getAzimuth(), EPS); } } diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/CircleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/CircleTest.java index 7b9ad53..8e96248 100644 --- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/CircleTest.java +++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/CircleTest.java @@ -99,10 +99,10 @@ public void testPhase() { @Test public void testSubSpace() { Circle circle = new Circle(S2Point.of(1.2, 2.5), S2Point.of(-4.3, 0), 1.0e-10); - Assert.assertEquals(0.0, circle.toSubSpace(S2Point.of(circle.getXAxis())).getAlpha(), 1.0e-10); - Assert.assertEquals(0.5 * Math.PI, circle.toSubSpace(S2Point.of(circle.getYAxis())).getAlpha(), 1.0e-10); + Assert.assertEquals(0.0, circle.toSubSpace(S2Point.of(circle.getXAxis())).getAzimuth(), 1.0e-10); + Assert.assertEquals(0.5 * Math.PI, circle.toSubSpace(S2Point.of(circle.getYAxis())).getAzimuth(), 1.0e-10); Vector3D p = Vector3D.of(1, 2, -4); - Assert.assertEquals(circle.getPhase(p), circle.toSubSpace(S2Point.of(p)).getAlpha(), 1.0e-10); + Assert.assertEquals(circle.getPhase(p), circle.toSubSpace(S2Point.of(p)).getAzimuth(), 1.0e-10); } @Test @@ -177,9 +177,9 @@ public void testTransform() { SubLimitAngle sub = new LimitAngle(S1Point.of(Geometry.TWO_PI * random.nextDouble()), random.nextBoolean(), 1.0e-10).wholeHyperplane(); - Vector3D psub = c.getPointAt(((LimitAngle) sub.getHyperplane()).getLocation().getAlpha()); + Vector3D psub = c.getPointAt(((LimitAngle) sub.getHyperplane()).getLocation().getAzimuth()); SubLimitAngle tsub = (SubLimitAngle) t.apply(sub, c, tc); - Vector3D ptsub = tc.getPointAt(((LimitAngle) tsub.getHyperplane()).getLocation().getAlpha()); + Vector3D ptsub = tc.getPointAt(((LimitAngle) tsub.getHyperplane()).getLocation().getAzimuth()); Assert.assertEquals(0.0, r.applyTo(psub).distance(ptsub), 1.0e-10); } diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/S2PointTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/S2PointTest.java index 2e15d09..827f107 100644 --- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/S2PointTest.java +++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/S2PointTest.java @@ -29,8 +29,8 @@ public void testS2Point() { for (int k = -2; k < 3; ++k) { S2Point p = S2Point.of(1.0 + k * Geometry.TWO_PI, 1.4); - Assert.assertEquals(1.0 + k * Geometry.TWO_PI, p.getTheta(), EPS); - Assert.assertEquals(1.4, p.getPhi(), EPS); + Assert.assertEquals(1.0, p.getAzimuth(), EPS); + Assert.assertEquals(1.4, p.getPolar(), EPS); Assert.assertEquals(Math.cos(1.0) * Math.sin(1.4), p.getVector().getX(), EPS); Assert.assertEquals(Math.sin(1.0) * Math.sin(1.4), p.getVector().getY(), EPS); Assert.assertEquals(Math.cos(1.4), p.getVector().getZ(), EPS); @@ -38,16 +38,6 @@ public void testS2Point() { } } - @Test(expected=IllegalArgumentException.class) - public void testNegativePolarAngle() { - S2Point.of(1.0, -1.0); - } - - @Test(expected=IllegalArgumentException.class) - public void testTooLargePolarAngle() { - S2Point.of(1.0, 3.5); - } - @Test public void testNaN() { Assert.assertTrue(S2Point.NaN.isNaN()); @@ -69,7 +59,7 @@ public void testEquals() { @Test public void testDistance() { S2Point a = S2Point.of(1.0, 0.5 * Math.PI); - S2Point b = S2Point.of(a.getTheta() + 0.5 * Math.PI, a.getPhi()); + S2Point b = S2Point.of(a.getAzimuth() + 0.5 * Math.PI, a.getPolar()); Assert.assertEquals(0.5 * Math.PI, a.distance(b), 1.0e-10); Assert.assertEquals(Math.PI, a.distance(a.negate()), 1.0e-10); Assert.assertEquals(0.5 * Math.PI, S2Point.MINUS_I.distance(S2Point.MINUS_K), 1.0e-10); @@ -97,7 +87,7 @@ public void testParse_failure() { } private void checkPoint(S2Point p, double theta, double phi) { - Assert.assertEquals(theta, p.getTheta(), EPS); - Assert.assertEquals(phi, p.getPhi(), EPS); + Assert.assertEquals(theta, p.getAzimuth(), EPS); + Assert.assertEquals(phi, p.getPolar(), EPS); } } diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSetTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSetTest.java index 0f056e8..ce6b231 100644 --- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSetTest.java +++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/SphericalPolygonsSetTest.java @@ -375,7 +375,7 @@ public void testPartWithHole() { SphericalPolygonsSet hexaWithHole = (SphericalPolygonsSet) new RegionFactory<S2Point>().difference(hexa, hole); - for (double phi = center.getPhi() - alpha + 0.1; phi < center.getPhi() + alpha - 0.1; phi += 0.07) { + for (double phi = center.getPolar() - alpha + 0.1; phi < center.getPolar() + alpha - 0.1; phi += 0.07) { Location l = hexaWithHole.checkPoint(S2Point.of(Math.PI / 4, phi)); if (phi < Math.PI / 6 || phi > Math.PI / 3) { Assert.assertEquals(Location.INSIDE, l); @@ -428,7 +428,7 @@ public void testConcentricSubParts() { concentric.getSize(), 1.0e-10); // we expect lots of sign changes as we traverse all concentric rings - double phi = S2Point.of(center).getPhi(); + double phi = S2Point.of(center).getPolar(); Assert.assertEquals(+0.207, concentric.projectToBoundary(S2Point.of(-0.60, phi)).getOffset(), 0.01); Assert.assertEquals(-0.048, concentric.projectToBoundary(S2Point.of(-0.21, phi)).getOffset(), 0.01); Assert.assertEquals(+0.027, concentric.projectToBoundary(S2Point.of(-0.10, phi)).getOffset(), 0.01); ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: us...@infra.apache.org With regards, Apache Git Services --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@commons.apache.org For additional commands, e-mail: dev-h...@commons.apache.org