You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2018/07/21 10:07:39 UTC

[commons-geometry] 01/15: GEOMETRY-7: adding PolarCoordinates class

This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit 76086cc615ce129629b2f5a4a91baa1c3c3b3467
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Thu Jun 21 23:39:15 2018 -0400

    GEOMETRY-7: adding PolarCoordinates class
---
 .../org/apache/commons/geometry/core/Geometry.java |  15 +-
 .../apache/commons/geometry/core/GeometryTest.java |  64 ++++
 .../geometry/euclidean/twod/Cartesian2D.java       |   7 +
 .../commons/geometry/euclidean/twod/Point2D.java   |   9 +
 .../geometry/euclidean/twod/PolarCoordinates.java  | 300 +++++++++++++++++
 .../commons/geometry/euclidean/twod/Vector2D.java  |   9 +
 .../geometry/euclidean/twod/Cartesian2DTest.java   |  33 ++
 .../geometry/euclidean/twod/Point2DTest.java       |  31 +-
 .../euclidean/twod/PolarCoordinatesTest.java       | 360 +++++++++++++++++++++
 .../geometry/euclidean/twod/Vector2DTest.java      |  30 +-
 10 files changed, 848 insertions(+), 10 deletions(-)

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