This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new ed08030194 feat(Geometry): review and add Bearing implementation
ed08030194 is described below
commit ed08030194c4ad01403285afabaa2a9262587950
Author: jsorel <[email protected]>
AuthorDate: Mon Sep 21 17:08:05 2026 +0200
feat(Geometry): review and add Bearing implementation
---
.../org.apache.sis.geometry/main/module-info.java | 1 +
.../main/org/apache/sis/geometries/Bearing.java | 51 ---
.../org/apache/sis/geometries/GeometryFactory.java | 1 +
.../main/org/apache/sis/geometries/Point.java | 1 +
.../main/org/apache/sis/geometries/cs/Bearing.java | 210 ++++++++++
.../sis/geometries/cs/CurveRelativeDirection.java | 51 ++-
.../apache/sis/geometries/cs/FixedDirection.java | 47 ++-
.../geometries/cs/GeometricCoordinateSystem.java | 1 -
.../sis/geometries/cs/ReferenceDirection.java | 23 +-
.../sis/geometries/cs/RelativeDirection.java | 38 +-
.../org/apache/sis/geometries/cs/Rotation.java | 22 +-
.../org/apache/sis/geometries/cs/package-info.java | 30 +-
.../apache/sis/geometries/curve/OffsetCurve.java | 2 +-
.../org/apache/sis/geometries/curve/Rhumb.java | 2 +-
.../geometries/internal/shared/DefaultBearing.java | 451 +++++++++++++++++++++
.../internal/shared/DefaultOffsetCurve.java | 2 +-
.../geometries/internal/shared/DefaultRhumb.java | 2 +-
.../org/apache/sis/geometries/cs/BearingTest.java | 438 ++++++++++++++++++++
18 files changed, 1297 insertions(+), 76 deletions(-)
diff --git a/incubator/src/org.apache.sis.geometry/main/module-info.java
b/incubator/src/org.apache.sis.geometry/main/module-info.java
index aea12d70e0..aec0fba718 100644
--- a/incubator/src/org.apache.sis.geometry/main/module-info.java
+++ b/incubator/src/org.apache.sis.geometry/main/module-info.java
@@ -32,6 +32,7 @@ module org.apache.sis.geometry {
exports org.apache.sis.images;
exports org.apache.sis.geometries;
exports org.apache.sis.geometries.adapter;
+ exports org.apache.sis.geometries.cs;
exports org.apache.sis.geometries.curve;
exports org.apache.sis.geometries.operation;
exports org.apache.sis.geometries.point;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Bearing.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Bearing.java
deleted file mode 100644
index 53dc37fe39..0000000000
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Bearing.java
+++ /dev/null
@@ -1,51 +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.sis.geometries;
-
-import static org.opengis.annotation.Specification.ISO_19107;
-import org.opengis.annotation.UML;
-
-
-/**
- * A direction at a point, expressed either as a set of angles or as a tangent
vector.
- *
- * <p>In the angular form, the first angle is an azimuth measured in the
tangent plane from a
- * reference direction, and the second one is an altitude, positive above the
horizontal and
- * negative below it. In the vector form, the direction is a unit vector of
the coordinate system at
- * the point. Both forms carry the same information; only their interpretation
differs, and that
- * interpretation depends on a reference direction giving the zero offset and
on a rotation
- * direction.</p>
- *
- * <p>Constraints:</p>
- * <ul>
- * <li>A bearing may be valid only at the point from which it is measured:
transporting a vector
- * to another point is valid only if the geometric reference surface is
planar.</li>
- * <li>A fixed reference direction such as true north allows some transport,
but only where that
- * reference exists and is unique. True north does not exist at the
North pole and is not
- * unique at the South pole.</li>
- * <li>The reference direction of a bearing shall not refer to that bearing
transitively.</li>
- * <li>The magnitude of the vector has no effect: only its direction
matters.</li>
- * </ul>
- *
- * @author Johann Sorel (Geomatys)
- *
- * @see ISO 19107:2019 - 6.2.22
- */
-@UML(identifier="Bearing", specification=ISO_19107)
-public interface Bearing {
-
-}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
index 1624106685..fa3612a6c4 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import javax.measure.Quantity;
import javax.measure.Unit;
+import org.apache.sis.geometries.cs.Bearing;
import org.apache.sis.geometries.cs.Projection;
import org.apache.sis.geometries.curve.Arc;
import org.apache.sis.geometries.curve.ArcByBulge;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java
index d26e71d250..7ebfec441c 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java
@@ -17,6 +17,7 @@
package org.apache.sis.geometries;
import java.util.List;
+import org.apache.sis.geometries.cs.Bearing;
import org.apache.sis.geometries.internal.shared.ArrayDataPoints;
import org.apache.sis.geometries.internal.shared.DefaultPoint;
import org.apache.sis.geometries.internal.shared.IndexedPoint;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/Bearing.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/Bearing.java
new file mode 100644
index 0000000000..fd66b57a10
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/Bearing.java
@@ -0,0 +1,210 @@
+/*
+ * 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.sis.geometries.cs;
+
+import javax.measure.Quantity;
+import javax.measure.quantity.Angle;
+import org.apache.sis.geometries.internal.shared.DefaultBearing;
+import org.apache.sis.maths.Vector;
+import static org.opengis.annotation.Specification.ISO_19107;
+import org.opengis.annotation.UML;
+
+
+/**
+ * A direction at a point, expressed either as a set of angles or as a tangent
vector.
+ *
+ * <p>In the angular form, the first angle is an azimuth measured in the
tangent plane from a
+ * reference direction, and the second one is an altitude, positive above the
horizontal and
+ * negative below it. In the vector form, the direction is a unit vector of
the coordinate system at
+ * the point. Both forms carry the same information; only their interpretation
differs, and that
+ * interpretation depends on a reference direction giving the zero offset and
on a rotation
+ * direction.</p>
+ *
+ * <p>Constraints:</p>
+ * <ul>
+ * <li>A bearing may be valid only at the point from which it is measured:
transporting a vector
+ * to another point is valid only if the geometric reference surface is
planar.</li>
+ * <li>A fixed reference direction such as true north allows some transport,
but only where that
+ * reference exists and is unique. True north does not exist at the
North pole and is not
+ * unique at the South pole.</li>
+ * <li>The reference direction of a bearing shall not refer to that bearing
transitively.</li>
+ * <li>The magnitude of the vector has no effect: only its direction
matters.</li>
+ * </ul>
+ *
+ * <p>Difference with ISO 19107: the {@code angle} attribute, an ordered list
of zero to two angles,
+ * is split into the two named accessors {@link #getAzimuth()} and {@link
#getAltitude()}, which are
+ * respectively its first and second element. An altitude is therefore present
only if an azimuth is
+ * present.</p>
+ *
+ * @author Johann Sorel (Geomatys)
+ *
+ * @see ISO 19107:2019 - 6.2.22
+ */
+@UML(identifier="Bearing", specification=ISO_19107)
+public interface Bearing extends ReferenceDirection {
+ /**
+ * Angle measured in the tangent plane from the {@linkplain
#getReference() reference direction},
+ * in the sense given by the {@linkplain #getRotation() rotation}, or
{@code null} if this bearing
+ * has no angular form.
+ *
+ * <p>The angle is returned in degrees, in the range of 0 inclusive to 360
exclusive. It is
+ * absent only when the direction vector has a dimension from which no
azimuth can be derived,
+ * which is any dimension other than 2 and 3.</p>
+ *
+ * @return azimuth of this bearing, or {@code null} if none.
+ *
+ * @see ISO 19107:2019 - 6.2.22.2
+ */
+ @UML(identifier="angle", specification=ISO_19107)
+ Quantity<Angle> getAzimuth();
+
+ /**
+ * Angle measured from the plane tangent to the reference surface,
positive above it and negative
+ * below it, or {@code null} if this bearing is confined to that plane.
+ *
+ * <p>The angle is returned in degrees, in the range of −90 to +90
inclusive. An altitude is
+ * present only if an {@linkplain #getAzimuth() azimuth} is present.</p>
+ *
+ * @return altitude of this bearing, or {@code null} if none.
+ *
+ * @see ISO 19107:2019 - 6.2.22.2
+ */
+ @UML(identifier="angle", specification=ISO_19107)
+ Quantity<Angle> getAltitude();
+
+ /**
+ * Direction of this bearing as a unit vector of the coordinate system at
the point.
+ *
+ * <p>Only the direction of that vector is significant; its length is
always one, whatever the
+ * length of the vector this bearing was created from.</p>
+ *
+ * @return direction of this bearing, or {@code null} if none.
+ *
+ * @see ISO 19107:2019 - 6.2.22.3
+ */
+ @UML(identifier="direction", specification=ISO_19107)
+ Vector<?> getDirection();
+
+ /**
+ * Direction from which the {@linkplain #getAzimuth() azimuth} is
measured, that is the direction
+ * of a zero angle.
+ *
+ * <p>Constraints:</p>
+ * <ul>
+ * <li>This direction shall not refer to this bearing transitively.</li>
+ * </ul>
+ *
+ * @return reference direction of this bearing, never null.
+ *
+ * @see ISO 19107:2019 - 6.2.22.4
+ */
+ @UML(identifier="reference", specification=ISO_19107)
+ ReferenceDirection getReference();
+
+ /**
+ * Sense in which the {@linkplain #getAzimuth() azimuth} is measured from
the
+ * {@linkplain #getReference() reference direction}.
+ *
+ * @return rotation sense of this bearing, never null.
+ *
+ * @see ISO 19107:2019 - 6.2.22.5
+ */
+ @UML(identifier="rotation", specification=ISO_19107)
+ Rotation getRotation();
+
+ /**
+ * Creates a bearing measured clockwise from true north by the given
azimuth.
+ *
+ * @param azimuth angle from the reference direction, not null.
+ * @return a bearing at the given azimuth.
+ * @throws IllegalArgumentException if the azimuth is not a finite angle.
+ *
+ * @see ISO 19107:2019 - 6.2.22.6
+ */
+ static Bearing ofAzimuth(final Quantity<Angle> azimuth) {
+ return ofAzimuth(azimuth, null, FixedDirection.TRUE_NORTH,
Rotation.CLOCKWISE);
+ }
+
+ /**
+ * Creates a bearing measured clockwise from true north by the given
azimuth and altitude.
+ *
+ * @param azimuth angle from the reference direction, not null.
+ * @param altitude angle above the tangent plane, or {@code null} if the
bearing lies in it.
+ * @return a bearing at the given angles.
+ * @throws IllegalArgumentException if an angle is not finite, or if the
altitude is outside
+ * the range of −90 to +90 degrees.
+ *
+ * @see ISO 19107:2019 - 6.2.22.6
+ */
+ static Bearing ofAzimuth(final Quantity<Angle> azimuth, final
Quantity<Angle> altitude) {
+ return ofAzimuth(azimuth, altitude, FixedDirection.TRUE_NORTH,
Rotation.CLOCKWISE);
+ }
+
+ /**
+ * Creates a bearing measured from the given reference direction in the
given sense.
+ *
+ * @param azimuth angle from the reference direction, not null.
+ * @param altitude angle above the tangent plane, or {@code null} if
the bearing lies in it.
+ * @param reference direction from which the azimuth is measured, not
null.
+ * @param rotation sense in which the azimuth is measured, not null.
+ * @return a bearing at the given angles.
+ * @throws IllegalArgumentException if an angle is not finite, if the
altitude is outside the
+ * range of −90 to +90 degrees, or if the reference direction
refers to the bearing
+ * being created.
+ *
+ * @see ISO 19107:2019 - 6.2.22.6
+ */
+ static Bearing ofAzimuth(final Quantity<Angle> azimuth, final
Quantity<Angle> altitude,
+ final ReferenceDirection reference, final
Rotation rotation)
+ {
+ return new DefaultBearing(azimuth, altitude, reference, rotation);
+ }
+
+ /**
+ * Creates a bearing in the direction of the given vector, measured
clockwise from true north.
+ *
+ * @param direction vector giving the direction of the bearing, not null.
+ * @return a bearing in the direction of the given vector.
+ * @throws IllegalArgumentException if the vector has a coordinate which
is not finite,
+ * or if its length is zero.
+ *
+ * @see ISO 19107:2019 - 6.2.22.6
+ */
+ static Bearing ofDirection(final Vector<?> direction) {
+ return ofDirection(direction, FixedDirection.TRUE_NORTH,
Rotation.CLOCKWISE);
+ }
+
+ /**
+ * Creates a bearing in the direction of the given vector, with the given
reference and sense.
+ * The reference direction and the rotation do not change the direction of
the bearing; they
+ * are the convention under which its {@linkplain #getAzimuth() azimuth}
is expressed.
+ *
+ * @param direction vector giving the direction of the bearing, not null.
+ * @param reference direction from which the azimuth is measured, not
null.
+ * @param rotation sense in which the azimuth is measured, not null.
+ * @return a bearing in the direction of the given vector.
+ * @throws IllegalArgumentException if the vector has a coordinate which
is not finite, if its
+ * length is zero, or if the reference direction refers to the
bearing being created.
+ *
+ * @see ISO 19107:2019 - 6.2.22.6
+ */
+ static Bearing ofDirection(final Vector<?> direction,
+ final ReferenceDirection reference, final
Rotation rotation)
+ {
+ return new DefaultBearing(direction, reference, rotation);
+ }
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/CurveRelativeDirection.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/CurveRelativeDirection.java
index be06486705..9615b910fd 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/CurveRelativeDirection.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/CurveRelativeDirection.java
@@ -21,19 +21,66 @@ import org.opengis.annotation.UML;
/**
+ * A reference direction relative to a curve, at a position on that curve.
+ *
+ * <p>These directions are the vectors of the local frame carried along the
curve. They vary from
+ * one position to another, so a {@linkplain Bearing bearing} measured from
one of them is meaningful
+ * only together with the position at which it is taken.</p>
*
* @author Johann Sorel (Geomatys)
+ *
+ * @see ISO 19107:2019 - 6.2.26
*/
-@UML(identifier="CurveRelativeDirection", specification=ISO_19107) // section
6.2.26
-public enum CurveRelativeDirection {
+@UML(identifier="CurveRelativeDirection", specification=ISO_19107)
+public enum CurveRelativeDirection implements ReferenceDirection {
+ /**
+ * The direction in which the curve is travelled, that is the unit vector
collinear with the
+ * derivative of the curve with respect to its arc length.
+ */
TANGENT,
+
+ /**
+ * Opposite to the {@linkplain #TANGENT tangent}.
+ */
REVERSE_TANGENT,
+
+ /**
+ * Perpendicular to the {@linkplain #TANGENT tangent}, in the direction of
the curvature vector.
+ */
NORMAL,
+
+ /**
+ * Opposite to the {@linkplain #NORMAL normal}.
+ */
REVERSE_NORMAL,
+
+ /**
+ * Toward the center of curvature, that is toward the inside of the curve.
+ */
BINORMAL,
+
+ /**
+ * Opposite to the {@linkplain #BINORMAL binormal}, that is toward the
outside of the curve.
+ */
REVERSE_BINORMAL,
+
+ /**
+ * Perpendicular to the {@linkplain #TANGENT tangent}, on its left side.
+ */
LEFT_NORMAL,
+
+ /**
+ * Perpendicular to the {@linkplain #TANGENT tangent}, on its right side.
+ */
RIGHT_NORMAL,
+
+ /**
+ * Perpendicular to the reference surface, pointing away from it.
+ */
UP_NORMAL,
+
+ /**
+ * Opposite to the {@linkplain #UP_NORMAL upward normal}.
+ */
DOWN_NORMAL
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/FixedDirection.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/FixedDirection.java
index b2b1003ae3..2693bf4d07 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/FixedDirection.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/FixedDirection.java
@@ -21,11 +21,52 @@ import org.opengis.annotation.UML;
/**
+ * A reference direction fixed with respect to the globe, a map, a coordinate
system or a grid.
+ *
+ * <p>Unlike a {@link RelativeDirection}, these directions do not depend on a
moving object, which
+ * makes a {@linkplain Bearing bearing} measured from one of them
transportable from one position to
+ * another. That transport holds only where the direction exists and is
unique: true north, for
+ * example, does not exist at the North pole and is not unique at the South
pole.</p>
+ *
+ * <p>Difference with ISO 19107: the standard declares this as a code list,
whose values may be
+ * extended by an application.</p>
*
* @author Johann Sorel (Geomatys)
+ *
+ * @see ISO 19107:2019 - 6.2.25
*/
-@UML(identifier="FixedDirection", specification=ISO_19107) // section 6.2.25
-public interface FixedDirection {
+@UML(identifier="FixedDirection", specification=ISO_19107)
+public enum FixedDirection implements ReferenceDirection {
+ /**
+ * Toward the geographic North pole, along the meridian of the position.
+ * This is the direction from which an azimuth is measured by default.
+ */
+ TRUE_NORTH,
+
+ /**
+ * Toward the magnetic North pole, as indicated by a compass needle.
+ * It differs from {@link #TRUE_NORTH} by the magnetic declination at the
position.
+ */
+ MAGNETIC_NORTH,
+
+ /**
+ * Toward the north of a grid, that is along the second axis of a
projected coordinate system.
+ * It differs from {@link #TRUE_NORTH} by the meridian convergence at the
position.
+ */
+ GRID_NORTH,
+
+ /**
+ * Opposite to {@link #TRUE_NORTH}.
+ */
+ TRUE_SOUTH,
+
+ /**
+ * Opposite to {@link #MAGNETIC_NORTH}.
+ */
+ MAGNETIC_SOUTH,
- //TODO
+ /**
+ * Opposite to {@link #GRID_NORTH}.
+ */
+ GRID_SOUTH
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/GeometricCoordinateSystem.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/GeometricCoordinateSystem.java
index 93e309f147..093087bc87 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/GeometricCoordinateSystem.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/GeometricCoordinateSystem.java
@@ -20,7 +20,6 @@ import javax.measure.Quantity;
import static org.opengis.annotation.Specification.ISO_19107;
import org.opengis.annotation.UML;
import org.opengis.geometry.DirectPosition;
-import org.apache.sis.geometries.Bearing;
import org.apache.sis.maths.Vector;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/ReferenceDirection.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/ReferenceDirection.java
index 7a0ad44305..ffe83e498c 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/ReferenceDirection.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/ReferenceDirection.java
@@ -21,10 +21,31 @@ import org.opengis.annotation.UML;
/**
+ * A direction serving as the origin from which a {@linkplain Bearing bearing}
is measured.
+ *
+ * <p>This interface declares no member: it exists to gather the types which
can play that role,
+ * and a type joins that set by implementing it. The types doing so in this
package are:</p>
+ * <ul>
+ * <li>{@link FixedDirection}, fixed with respect to the globe, a map or a
grid;</li>
+ * <li>{@link RelativeDirection}, relative to a moving object;</li>
+ * <li>{@link CurveRelativeDirection}, relative to a curve at a position on
it;</li>
+ * <li>{@link Bearing} itself, which makes the definition recursive: a
bearing may be measured
+ * from another bearing. ISO 19107 bounds that recursion by requiring
the reference direction
+ * of a bearing to not refer to that bearing transitively.</li>
+ * </ul>
+ *
+ * <p>Difference with ISO 19107: the standard defines this as an empty
interface which shall be
+ * implemented by any datatype able to represent a direction at a position. It
is therefore left
+ * open here rather than sealed over the four types above, so that an
application may contribute
+ * its own. A consequence is that the recursion constraint cannot be enforced
by the type system
+ * and is checked when a bearing is created instead.</p>
*
* @author Johann Sorel (Geomatys)
+ *
+ * @see Bearing#getReference()
+ * @see ISO 19107:2019 - 6.2.21, 6.2.22.4
*/
-@UML(identifier="ReferenceDirection", specification=ISO_19107) // section
6.2.22.4
+@UML(identifier="ReferenceDirection", specification=ISO_19107)
public interface ReferenceDirection {
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/RelativeDirection.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/RelativeDirection.java
index 5c097ff36d..dd651fb4b3 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/RelativeDirection.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/RelativeDirection.java
@@ -21,13 +21,39 @@ import org.opengis.annotation.UML;
/**
+ * A reference direction relative to a moving object, such as a vehicle.
+ *
+ * <p>These directions are carried by the object and turn with it: they are
meaningful only while
+ * the heading of that object is known. A {@linkplain Bearing bearing}
measured from one of them is
+ * therefore relative, unlike a bearing measured from a {@link
FixedDirection}.</p>
*
* @author Johann Sorel (Geomatys)
+ *
+ * @see ISO 19107:2019 - 6.2.24
*/
-@UML(identifier="RelativeDirection", specification=ISO_19107) // section 6.2.24
-public enum RelativeDirection {
- FORWARD, //FORE
- BACKWARD, //AFT
- LEFT, //PORT
- RIGHT //STARBOARD
+@UML(identifier="RelativeDirection", specification=ISO_19107)
+public enum RelativeDirection implements ReferenceDirection {
+ /**
+ * Toward the front of the object, in the direction of its movement.
+ * Also called <dfn>fore</dfn>.
+ */
+ FORWARD,
+
+ /**
+ * Toward the rear of the object, opposite to the direction of its
movement.
+ * Also called <dfn>aft</dfn>.
+ */
+ BACKWARD,
+
+ /**
+ * Ninety degrees to the left of {@link #FORWARD}.
+ * Also called <dfn>port</dfn>.
+ */
+ LEFT,
+
+ /**
+ * Ninety degrees to the right of {@link #FORWARD}.
+ * Also called <dfn>starboard</dfn>.
+ */
+ RIGHT
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/Rotation.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/Rotation.java
index a00756457d..7e6d1a8151 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/Rotation.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/Rotation.java
@@ -21,11 +21,31 @@ import org.opengis.annotation.UML;
/**
+ * The sense in which an angular measure increases.
+ *
+ * <p>The two senses are named as seen from the positive side of the normal to
the surface on which
+ * the angle is measured, that is, looking down on that surface from above.
This is the convention
+ * of a compass laid flat on the ground and read from above.</p>
+ *
+ * <p>A rotation is not a direction: it says how an angle grows, not where it
starts. The origin of
+ * the measure is given by a {@link ReferenceDirection} instead.</p>
*
* @author Johann Sorel (Geomatys)
+ *
+ * @see Bearing#getRotation()
+ * @see ISO 19107:2019 - 6.2.23
*/
-@UML(identifier="Rotation", specification=ISO_19107) // section 6.2.23
+@UML(identifier="Rotation", specification=ISO_19107)
public enum Rotation {
+ /**
+ * Angles increase in the direction followed by the hands of a clock.
+ * This is the usual sense of a compass azimuth, which grows from north
toward east.
+ */
CLOCKWISE,
+
+ /**
+ * Angles increase in the direction opposite to the hands of a clock.
+ * This is the usual sense of trigonometry, which grows from the first
axis toward the second.
+ */
COUNTER_CLOCKWISE
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/package-info.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/package-info.java
index 66d0a3be06..d16169187f 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/package-info.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/cs/package-info.java
@@ -1,13 +1,29 @@
+/*
+ * 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.
+ */
/**
+ * Coordinate systems in which geometries are defined, and the directions
measured within them.
*
- * <h2>Remaining work to be done</h2>
- *
- * <ul>
- * <li>Complete package is draft and needs to be reviewed with Apache SIS
and GeoAPI</li>
- * <li>Review GeoAPI DirectPosition, section 6.2.9</li>
- * <li>Implement Vector, section 6.2.27</li>
- * </ul>
+ * <p>This package implements the <cite>Coordinate</cite> requirements class
of ISO 19107:2019
+ * (clause 6.2): the {@linkplain
org.apache.sis.geometries.cs.GeometricCoordinateSystem coordinate
+ * system} on which geometric measures are computed, the {@linkplain
org.apache.sis.geometries.cs.Bearing
+ * bearing} which expresses a direction at a position, and the code lists of
reference directions
+ * from which a bearing can be measured.</p>
*
+ * @author Johann Sorel (Geomatys)
*/
package org.apache.sis.geometries.cs;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/OffsetCurve.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/OffsetCurve.java
index d1baf8a882..01ab0403db 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/OffsetCurve.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/OffsetCurve.java
@@ -17,7 +17,7 @@
package org.apache.sis.geometries.curve;
import javax.measure.Quantity;
-import org.apache.sis.geometries.Bearing;
+import org.apache.sis.geometries.cs.Bearing;
import org.apache.sis.geometries.Curve;
import org.apache.sis.geometries.GeometryType;
import org.apache.sis.geometries.internal.shared.DefaultOffsetCurve;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Rhumb.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Rhumb.java
index 02c12b671a..83fb0a9c5c 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Rhumb.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Rhumb.java
@@ -16,7 +16,7 @@
*/
package org.apache.sis.geometries.curve;
-import org.apache.sis.geometries.Bearing;
+import org.apache.sis.geometries.cs.Bearing;
import org.apache.sis.geometries.Curve;
import org.apache.sis.geometries.CurveInterpolation;
import org.apache.sis.geometries.GeometryType;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBearing.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBearing.java
new file mode 100644
index 0000000000..51b4bc773c
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBearing.java
@@ -0,0 +1,451 @@
+/*
+ * 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.sis.geometries.internal.shared;
+
+import java.util.Arrays;
+import java.util.Objects;
+import javax.measure.Quantity;
+import javax.measure.quantity.Angle;
+import org.apache.sis.geometries.cs.Bearing;
+import org.apache.sis.geometries.cs.ReferenceDirection;
+import org.apache.sis.geometries.cs.Rotation;
+import org.apache.sis.maths.DataType;
+import org.apache.sis.maths.SampleSystem;
+import org.apache.sis.maths.Vector;
+import org.apache.sis.maths.Vectors;
+import org.apache.sis.measure.Quantities;
+import org.apache.sis.measure.Units;
+import org.apache.sis.referencing.internal.shared.AxisDirections;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.cs.CoordinateSystem;
+
+
+/**
+ * A direction at a point, stored in both its angular and its vectorial form.
+ *
+ * <p>Whichever form is given at construction, the other one is derived
immediately when the
+ * coordinate system allows it, so that both accessors can answer without
further computation.
+ * Instances are immutable.</p>
+ *
+ * <h2>Conventions</h2>
+ * The azimuth is measured in the plane of the <var>east</var> and
<var>north</var> axes, from
+ * <var>north</var> toward <var>east</var> when the rotation is {@linkplain
Rotation#CLOCKWISE
+ * clockwise}, and is expressed in degrees in the range of 0 inclusive to 360
exclusive. The
+ * altitude is measured from that plane toward the <var>up</var> axis, in
degrees in the range of
+ * −90 to +90 inclusive. The rotation affects the azimuth only: the sign of
the altitude is fixed
+ * by the up axis.
+ *
+ * <p>Those three axes are located by their {@linkplain AxisDirection
direction} in the coordinate
+ * reference system of the direction vector, so that a bearing has the same
azimuth whether its
+ * vector is expressed in a (<var>latitude</var>, <var>longitude</var>) or in a
+ * (<var>longitude</var>, <var>latitude</var>) system. When that system is
absent or has no such
+ * axes, the coordinates are taken in the order in which they are given.</p>
+ *
+ * <p>Angles can be derived only from a vector of dimension 2 or 3. In any
other dimension the
+ * mapping from coordinates to a horizontal plane is not known — a fourth axis
may be time or a
+ * parameter — so both angles are absent and only the direction is
available.</p>
+ *
+ * @author Johann Sorel (Geomatys)
+ *
+ * @see ISO 19107:2019 - 6.2.22
+ */
+public final class DefaultBearing implements Bearing {
+ /**
+ * Maximum number of nested reference directions.
+ * ISO 19107 requires the reference direction of a bearing to not refer to
that bearing
+ * transitively (REQ. 44). Because an implementation of {@link Bearing}
other than this one may
+ * return a new instance on each call to {@link Bearing#getReference()},
the chain is bounded by
+ * a maximum depth.
+ */
+ private static final int MAX_REFERENCE_DEPTH = 100;
+
+ /**
+ * Angle from the reference direction, in degrees in the range of 0
inclusive to 360 exclusive,
+ * or {@link Double#NaN} if this bearing has no angular form.
+ */
+ private final double azimuth;
+
+ /**
+ * Angle above the horizontal plane, in degrees in the range of −90 to +90
inclusive,
+ * or {@link Double#NaN} if this bearing lies in that plane or has no
angular form.
+ */
+ private final double altitude;
+
+ /**
+ * Direction of this bearing as a unit vector. Never null and never
exposed directly,
+ * a copy being returned instead.
+ */
+ private final double[] coordinates;
+
+ /**
+ * The system in which {@link #coordinates} are expressed. Never null.
+ */
+ private final SampleSystem system;
+
+ /**
+ * Direction from which {@link #azimuth} is measured. Never null.
+ */
+ private final ReferenceDirection reference;
+
+ /**
+ * Sense in which {@link #azimuth} is measured. Never null.
+ */
+ private final Rotation rotation;
+
+ /**
+ * Creates a bearing from its angular form, deriving its direction vector.
+ * The derived vector has two coordinates if the altitude is null and
three otherwise,
+ * given in the <var>east</var>, <var>north</var>, <var>up</var> order.
+ *
+ * @param azimuth angle from the reference direction, not null.
+ * @param altitude angle above the horizontal plane, or {@code null} if
the bearing lies in it.
+ * @param reference direction from which the azimuth is measured, not
null.
+ * @param rotation sense in which the azimuth is measured, not null.
+ * @throws IllegalArgumentException if an angle is not finite, if the
altitude is outside the
+ * range of −90 to +90 degrees, or if the reference direction
refers to this bearing.
+ */
+ public DefaultBearing(final Quantity<Angle> azimuth, final Quantity<Angle>
altitude,
+ final ReferenceDirection reference, final Rotation
rotation)
+ {
+ ArgumentChecks.ensureNonNull("azimuth", azimuth);
+ ArgumentChecks.ensureNonNull("rotation", rotation);
+ this.reference = validate(reference);
+ this.rotation = rotation;
+ this.azimuth = normalizeDegrees(degrees("azimuth", azimuth));
+ if (altitude == null) {
+ this.altitude = Double.NaN;
+ } else {
+ final double h = degrees("altitude", altitude);
+ if (!(h >= -90 && h <= 90)) {
+ throw new IllegalArgumentException("The altitude of a bearing
shall be between "
+ + "-90° and +90°, but got " + h + "°.");
+ }
+ this.altitude = h;
+ }
+ /*
+ * Derive the direction. The azimuth turns from north toward east,
which is the direction
+ * of decreasing angle in the trigonometric sense, hence the sine on
the east coordinate
+ * and the cosine on the north one.
+ */
+ double a = Math.toRadians(this.azimuth);
+ if (rotation == Rotation.COUNTER_CLOCKWISE) {
+ a = -a;
+ }
+ if (Double.isNaN(this.altitude)) {
+ coordinates = new double[] {Math.sin(a), Math.cos(a)};
+ } else {
+ final double h = Math.toRadians(this.altitude);
+ final double c = Math.cos(h);
+ coordinates = new double[] {c * Math.sin(a), c * Math.cos(a),
Math.sin(h)};
+ }
+ system = SampleSystem.ofSize(coordinates.length);
+ }
+
+ /**
+ * Creates a bearing from a direction vector, deriving its angular form
when the dimension of
+ * that vector allows it. Only the direction of the given vector is
retained; its length is
+ * normalized to one.
+ *
+ * @param direction vector giving the direction of the bearing, not null.
+ * @param reference direction from which the azimuth is measured, not
null.
+ * @param rotation sense in which the azimuth is measured, not null.
+ * @throws IllegalArgumentException if the vector has a coordinate which
is not finite, if its
+ * length is zero, or if the reference direction refers to this
bearing.
+ */
+ public DefaultBearing(final Vector<?> direction, final ReferenceDirection
reference, final Rotation rotation) {
+ ArgumentChecks.ensureNonNull("direction", direction);
+ ArgumentChecks.ensureNonNull("rotation", rotation);
+ this.reference = validate(reference);
+ this.rotation = rotation;
+ /*
+ * `toArrayDouble()` returns a new array, which gives the defensive
copy for free and at
+ * full precision. The vector is not copied then normalized, because a
copy keeps the data
+ * type of its source and normalizing an integer vector would round
every coordinate.
+ */
+ final double[] c = direction.toArrayDouble();
+ for (final double v : c) {
+ if (!Double.isFinite(v)) {
+ throw new IllegalArgumentException("The direction of a bearing
shall have finite "
+ + "coordinates, but got " + Arrays.toString(c) + '.');
+ }
+ }
+ final double length = Math.sqrt(Vectors.lengthSquare(c));
+ if (!(length > 0)) {
+ throw new IllegalArgumentException("The direction of a bearing
shall not have a length "
+ + "of zero, because only its direction is significant.");
+ }
+ for (int i=0; i<c.length; i++) {
+ c[i] /= length;
+ }
+ coordinates = c;
+ final SampleSystem s = direction.getSampleSystem();
+ system = (s != null) ? s : SampleSystem.ofSize(c.length);
+ /*
+ * Derive the angles. `atan2(east, north)` and not `atan2(north,
east)`, because the angle
+ * is measured from the north axis: north gives 0°, east 90°, south
180° and west 270°.
+ * At the zenith and at the nadir the azimuth is undefined;
`atan2(0,0)` is zero, so it is
+ * arbitrarily reported as 0°.
+ */
+ final int[] axes = horizontalAxes(system, c.length);
+ if (axes == null) {
+ azimuth = Double.NaN;
+ altitude = Double.NaN;
+ } else {
+ double a = Math.toDegrees(Math.atan2(coordinate(c, axes[0]),
coordinate(c, axes[1])));
+ if (rotation == Rotation.COUNTER_CLOCKWISE) {
+ a = -a;
+ }
+ azimuth = normalizeDegrees(a);
+ if (axes.length < 3) {
+ altitude = Double.NaN;
+ } else {
+ final double z = coordinate(c, axes[2]);
+ altitude = Math.toDegrees(Math.asin(Math.max(-1, Math.min(1,
z))));
+ }
+ }
+ }
+
+ /**
+ * Returns the value of the given angle in degrees.
+ *
+ * @param name name of the argument, for the error message.
+ * @param angle the angle to convert, not null.
+ * @return the given angle in degrees.
+ * @throws IllegalArgumentException if the angle is not finite.
+ */
+ private static double degrees(final String name, final Quantity<Angle>
angle) {
+ final double value = angle.to(Units.DEGREE).getValue().doubleValue();
+ if (!Double.isFinite(value)) {
+ throw new IllegalArgumentException("The " + name + " of a bearing
shall be a finite "
+ + "angle, but got " + angle + '.');
+ }
+ return value;
+ }
+
+ /**
+ * Returns the given angle in the range of 0 inclusive to 360 exclusive.
+ *
+ * @param angle the angle to reduce, in degrees.
+ * @return the given angle reduced to one turn.
+ */
+ private static double normalizeDegrees(double angle) {
+ angle %= 360;
+ if (angle < 0) {
+ angle += 360;
+ }
+ if (angle >= 360) {
+ /*
+ * A very small negative angle added to 360 may round up to
exactly 360.
+ */
+ angle = 0;
+ }
+ /*
+ * Turn a negative zero into a positive one: the remainder of a
negative zero is a negative
+ * zero, which compares as a different value in `equals(Object)`.
+ */
+ return angle + 0.0;
+ }
+
+ /**
+ * Verifies that the given reference direction does not refer to the
bearing being created.
+ *
+ * @param reference the reference direction to verify, not null.
+ * @return the given reference direction.
+ * @throws IllegalArgumentException if the chain of reference directions
is too deep,
+ * which is the case in particular if it contains a cycle.
+ */
+ private static ReferenceDirection validate(final ReferenceDirection
reference) {
+ ArgumentChecks.ensureNonNull("reference", reference);
+ int depth = 0;
+ for (ReferenceDirection r = reference; r instanceof Bearing b;) {
+ if (++depth > MAX_REFERENCE_DEPTH) {
+ throw new IllegalArgumentException("The reference direction of
a bearing shall not "
+ + "refer to that bearing transitively, but the chain
of reference directions "
+ + "given as \"reference\" is more than " +
MAX_REFERENCE_DEPTH + " levels deep.");
+ }
+ r = b.getReference();
+ }
+ return reference;
+ }
+
+ /**
+ * Returns the indices of the east, north and, in three dimensions, up
axes of the given system.
+ * A negative value <var>v</var> means that the axis at index {@code -1-v}
points the opposite
+ * way, so that its coordinate shall be negated.
+ *
+ * @param system the system in which the coordinates are expressed,
not null.
+ * @param dimension number of coordinates.
+ * @return indices of the east, north and up axes, or {@code null} if no
angle can be derived.
+ */
+ private static int[] horizontalAxes(final SampleSystem system, final int
dimension) {
+ if (dimension < 2 || dimension > 3) {
+ return null;
+ }
+ final CoordinateReferenceSystem crs =
system.getCoordinateReferenceSystem();
+ if (crs != null) {
+ final CoordinateSystem cs = crs.getCoordinateSystem();
+ final int east = indexOf(cs, AxisDirection.EAST, dimension);
+ final int north = indexOf(cs, AxisDirection.NORTH, dimension);
+ if (east != Integer.MIN_VALUE && north != Integer.MIN_VALUE) {
+ if (dimension < 3) {
+ return new int[] {east, north};
+ }
+ final int up = indexOf(cs, AxisDirection.UP, dimension);
+ if (up != Integer.MIN_VALUE) {
+ return new int[] {east, north, up};
+ }
+ }
+ }
+ /*
+ * No coordinate reference system, or no such axes in it. This is the
case of the vectors
+ * derived from an angular bearing, and of the engineering systems
used for geometries
+ * which are not georeferenced. Take the coordinates in the order they
are given.
+ */
+ return (dimension < 3) ? new int[] {0, 1} : new int[] {0, 1, 2};
+ }
+
+ /**
+ * Returns the index of the axis having the given direction, negated as
{@code -1-index}
+ * if that axis points the opposite way.
+ *
+ * @param cs the coordinate system to inspect, not null.
+ * @param direction the direction of the axis to search.
+ * @param dimension number of coordinates available.
+ * @return the encoded index, or {@link Integer#MIN_VALUE} if no usable
axis was found.
+ */
+ private static int indexOf(final CoordinateSystem cs, final AxisDirection
direction, final int dimension) {
+ final int i = AxisDirections.indexOfColinear(cs, direction);
+ if (i < 0 || i >= dimension) {
+ return Integer.MIN_VALUE;
+ }
+ return direction.equals(cs.getAxis(i).getDirection()) ? i : (-1 - i);
+ }
+
+ /**
+ * Returns the coordinate designated by the given index, as encoded by
+ * {@link #indexOf(CoordinateSystem, AxisDirection, int)}.
+ *
+ * @param coordinates the coordinates from which to read.
+ * @param index the encoded index of the coordinate to read.
+ * @return the designated coordinate, negated if its axis points the
opposite way.
+ */
+ private static double coordinate(final double[] coordinates, final int
index) {
+ return (index >= 0) ? coordinates[index] : -coordinates[-1 - index];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Quantity<Angle> getAzimuth() {
+ return Double.isNaN(azimuth) ? null : Quantities.create(azimuth,
Units.DEGREE);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Quantity<Angle> getAltitude() {
+ return Double.isNaN(altitude) ? null : Quantities.create(altitude,
Units.DEGREE);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This method returns a new vector on each call, because vectors are
mutable.
+ * Modifying the returned vector has no effect on this bearing.</p>
+ */
+ @Override
+ public Vector<?> getDirection() {
+ final Vector<?> v = Vectors.create(system, DataType.DOUBLE);
+ for (int i=0; i<coordinates.length; i++) {
+ v.set(i, coordinates[i]);
+ }
+ return v;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReferenceDirection getReference() {
+ return reference;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Rotation getRotation() {
+ return rotation;
+ }
+
+ /**
+ * Compares this bearing with the given object for equality.
+ * Values are compared exactly, with no tolerance, so a bearing created
from angles and a
+ * bearing created from an equivalent direction vector are not necessarily
equal.
+ *
+ * @param other the object to compare with this bearing, or {@code null}.
+ * @return whether the given object is a bearing equal to this one.
+ */
+ @Override
+ public boolean equals(final Object other) {
+ if (!(other instanceof DefaultBearing)) {
+ return false;
+ }
+ final DefaultBearing that = (DefaultBearing) other;
+ return Double.doubleToLongBits(azimuth) ==
Double.doubleToLongBits(that.azimuth)
+ && Double.doubleToLongBits(altitude) ==
Double.doubleToLongBits(that.altitude)
+ && Arrays.equals(coordinates, that.coordinates)
+ && system.equals(that.system)
+ && reference.equals(that.reference)
+ && rotation == that.rotation;
+ }
+
+ /**
+ * Returns a hash code value for this bearing.
+ *
+ * @return a hash code value.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(azimuth, altitude, system, reference, rotation) ^
Arrays.hashCode(coordinates);
+ }
+
+ /**
+ * Returns a string representation of this bearing for debugging purposes.
+ * The format is not guaranteed to remain the same in future versions.
+ *
+ * @return a string representation of this bearing.
+ */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("Bearing[");
+ if (!Double.isNaN(azimuth)) {
+ sb.append("azimuth=").append(azimuth).append("°, ");
+ if (!Double.isNaN(altitude)) {
+ sb.append("altitude=").append(altitude).append("°, ");
+ }
+ }
+ return sb.append("direction=").append(Arrays.toString(coordinates))
+ .append(", reference=").append(reference)
+ .append(", rotation=").append(rotation)
+ .append(']').toString();
+ }
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultOffsetCurve.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultOffsetCurve.java
index 85e1fce05f..a51f38c7fc 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultOffsetCurve.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultOffsetCurve.java
@@ -18,7 +18,7 @@ package org.apache.sis.geometries.internal.shared;
import java.util.Objects;
import javax.measure.Quantity;
-import org.apache.sis.geometries.Bearing;
+import org.apache.sis.geometries.cs.Bearing;
import org.apache.sis.geometries.Curve;
import org.apache.sis.geometries.DataPoints;
import org.apache.sis.geometries.curve.OffsetCurve;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRhumb.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRhumb.java
index 948ae7eb93..4c4f17a822 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRhumb.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRhumb.java
@@ -16,7 +16,7 @@
*/
package org.apache.sis.geometries.internal.shared;
-import org.apache.sis.geometries.Bearing;
+import org.apache.sis.geometries.cs.Bearing;
import org.apache.sis.geometries.DataPoints;
import org.apache.sis.geometries.curve.Rhumb;
import org.opengis.geometry.Envelope;
diff --git
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/cs/BearingTest.java
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/cs/BearingTest.java
new file mode 100644
index 0000000000..b98eafea72
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/cs/BearingTest.java
@@ -0,0 +1,438 @@
+/*
+ * 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.sis.geometries.cs;
+
+import java.util.Arrays;
+import javax.measure.Quantity;
+import javax.measure.quantity.Angle;
+import org.apache.sis.maths.Vector;
+import org.apache.sis.maths.Vectors;
+import org.apache.sis.measure.Quantities;
+import org.apache.sis.measure.Units;
+import org.apache.sis.referencing.CommonCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+// Test dependencies
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+
+/**
+ * Tests {@link Bearing}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class BearingTest {
+ /**
+ * Tolerance for the comparison of angles in degrees and of vector
coordinates.
+ */
+ private static final double TOLERANCE = 1E-9;
+
+ /**
+ * Creates a new test case.
+ */
+ public BearingTest() {
+ }
+
+ /**
+ * Returns the given value as an angle in degrees.
+ */
+ private static Quantity<Angle> deg(final double value) {
+ return Quantities.create(value, Units.DEGREE);
+ }
+
+ /**
+ * Creates a vector with the given coordinates, in no particular
coordinate reference system.
+ */
+ private static Vector<?> vector(final double... values) {
+ final Vector<?> v = Vectors.createDouble(values.length);
+ for (int i=0; i<values.length; i++) {
+ v.set(i, values[i]);
+ }
+ return v;
+ }
+
+ /**
+ * Creates a vector with the given coordinates in the given coordinate
reference system.
+ */
+ private static Vector<?> vector(final CoordinateReferenceSystem crs, final
double... values) {
+ final Vector<?> v = Vectors.createDouble(crs);
+ for (int i=0; i<values.length; i++) {
+ v.set(i, values[i]);
+ }
+ return v;
+ }
+
+ /**
+ * Returns the azimuth of the given bearing in degrees.
+ */
+ private static double azimuth(final Bearing bearing) {
+ final Quantity<Angle> a = bearing.getAzimuth();
+ assertNotNull(a, "azimuth");
+ assertEquals(Units.DEGREE, a.getUnit(), "Angles shall be returned in
degrees.");
+ return a.getValue().doubleValue();
+ }
+
+ /**
+ * Returns the altitude of the given bearing in degrees.
+ */
+ private static double altitude(final Bearing bearing) {
+ final Quantity<Angle> a = bearing.getAltitude();
+ assertNotNull(a, "altitude");
+ assertEquals(Units.DEGREE, a.getUnit(), "Angles shall be returned in
degrees.");
+ return a.getValue().doubleValue();
+ }
+
+ /**
+ * A bearing whose angular form is given, together with the direction
vector expected from it.
+ *
+ * @param azimuth angle from the reference direction, in degrees.
+ * @param altitude angle above the horizontal plane in degrees, or
{@code null} if none.
+ * @param rotation sense in which the azimuth is measured.
+ * @param expected expected coordinates of the direction vector.
+ */
+ private record TestCase(double azimuth, Double altitude, Rotation
rotation, double[] expected) {
+ }
+
+ /**
+ * The bearings tested by {@link #testAnglesToDirection()} and {@link
#testDirectionToAngles()}.
+ * The coordinates are given in the east, north and up order.
+ */
+ private static final TestCase[] ENTRIES = {
+ new TestCase( 0, null, Rotation.CLOCKWISE, new double[] { 0,
1}),
+ new TestCase( 90, null, Rotation.CLOCKWISE, new double[] { 1,
0}),
+ new TestCase(180, null, Rotation.CLOCKWISE, new double[] { 0,
-1}),
+ new TestCase(270, null, Rotation.CLOCKWISE, new double[] {-1,
0}),
+ new TestCase( 90, null, Rotation.COUNTER_CLOCKWISE, new double[] {-1,
0}),
+ new TestCase(270, null, Rotation.COUNTER_CLOCKWISE, new double[] { 1,
0}),
+ new TestCase( 0, 0.0, Rotation.CLOCKWISE, new double[] { 0,
1, 0}),
+ new TestCase( 90, 0.0, Rotation.CLOCKWISE, new double[] { 1,
0, 0}),
+ new TestCase( 0, 45.0, Rotation.CLOCKWISE, new double[] { 0,
Math.sqrt(0.5), Math.sqrt(0.5)}),
+ new TestCase( 45, 0.0, Rotation.CLOCKWISE, new double[] {
Math.sqrt(0.5), Math.sqrt(0.5), 0})
+ };
+
+ /**
+ * Test of {@code ofAzimuth(Quantity)} default values.
+ */
+ @Test
+ public void testDefaults() {
+ final Bearing bearing = Bearing.ofAzimuth(deg(45));
+ assertEquals(45, azimuth(bearing), TOLERANCE);
+ assertNull(bearing.getAltitude(), "A bearing created without altitude
shall have none.");
+ assertEquals(FixedDirection.TRUE_NORTH, bearing.getReference());
+ assertEquals(Rotation.CLOCKWISE, bearing.getRotation());
+ assertEquals(2, bearing.getDirection().getDimension());
+ }
+
+ /**
+ * Test of the direction derived from the angles of a bearing.
+ */
+ @Test
+ public void testAnglesToDirection() {
+ for (final TestCase entry : ENTRIES) {
+ final Bearing bearing = Bearing.ofAzimuth(deg(entry.azimuth()),
+ (entry.altitude() != null) ? deg(entry.altitude()) : null,
+ FixedDirection.TRUE_NORTH, entry.rotation());
+ assertArrayEquals(entry.expected(),
bearing.getDirection().toArrayDouble(), TOLERANCE,
+ () -> "Direction of azimuth " + entry.azimuth() + "° " +
entry.rotation());
+ }
+ }
+
+ /**
+ * Test of the angles derived from the direction of a bearing.
+ */
+ @Test
+ public void testDirectionToAngles() {
+ for (final TestCase entry : ENTRIES) {
+ final Bearing bearing =
Bearing.ofDirection(vector(entry.expected()),
+ FixedDirection.TRUE_NORTH, entry.rotation());
+ assertEquals(entry.azimuth(), azimuth(bearing), TOLERANCE,
+ () -> "Azimuth of " + Arrays.toString(entry.expected()));
+ if (entry.altitude() == null) {
+ assertNull(bearing.getAltitude());
+ } else {
+ assertEquals(entry.altitude(), altitude(bearing), TOLERANCE);
+ }
+ }
+ }
+
+ /**
+ * Test that the angular and the vectorial forms convert into each other
without loss.
+ */
+ @Test
+ public void testRoundTrip() {
+ for (final double a : new double[] {0, 1, 45, 90, 180, 270, 359}) {
+ for (final Double h : new Double[] {null, -89.0, -45.0, 0.0, 45.0,
89.0}) {
+ final Bearing source = Bearing.ofAzimuth(deg(a), (h != null) ?
deg(h) : null);
+ final Bearing target =
Bearing.ofDirection(source.getDirection());
+ assertEquals(a, azimuth(target), TOLERANCE, () -> "Azimuth " +
a + "°, altitude " + h);
+ if (h == null) {
+ assertNull(target.getAltitude());
+ } else {
+ assertEquals(h, altitude(target), TOLERANCE);
+ }
+ }
+ }
+ }
+
+ /**
+ * Test that the rotation changes the azimuth but not the altitude.
+ */
+ @Test
+ public void testRotation() {
+ final Bearing cw = Bearing.ofAzimuth(deg(90), deg(30),
FixedDirection.TRUE_NORTH, Rotation.CLOCKWISE);
+ final Bearing ccw = Bearing.ofAzimuth(deg(90), deg(30),
FixedDirection.TRUE_NORTH, Rotation.COUNTER_CLOCKWISE);
+ final double[] d1 = cw .getDirection().toArrayDouble();
+ final double[] d2 = ccw.getDirection().toArrayDouble();
+ assertEquals(-d1[0], d2[0], TOLERANCE, "The east coordinate shall be
mirrored.");
+ assertEquals( d1[1], d2[1], TOLERANCE, "The north coordinate shall be
unchanged.");
+ assertEquals( d1[2], d2[2], TOLERANCE, "The rotation shall not change
the altitude.");
+ assertEquals(30, altitude(ccw), TOLERANCE);
+ /*
+ * The same direction read in the two senses gives supplementary
azimuths.
+ */
+ final Vector<?> east = vector(1, 0);
+ assertEquals( 90, azimuth(Bearing.ofDirection(east,
FixedDirection.TRUE_NORTH, Rotation.CLOCKWISE)), TOLERANCE);
+ assertEquals(270, azimuth(Bearing.ofDirection(east,
FixedDirection.TRUE_NORTH, Rotation.COUNTER_CLOCKWISE)), TOLERANCE);
+ }
+
+ /**
+ * Test that the azimuth is reduced to the range of 0 inclusive to 360
exclusive.
+ */
+ @Test
+ public void testAzimuthNormalization() {
+ assertEquals(270, azimuth(Bearing.ofAzimuth(deg(-90))), TOLERANCE);
+ assertEquals( 90, azimuth(Bearing.ofAzimuth(deg(450))), TOLERANCE);
+ assertEquals( 0, azimuth(Bearing.ofAzimuth(deg(720))), TOLERANCE);
+ assertEquals(359, azimuth(Bearing.ofAzimuth(deg(-1))), TOLERANCE);
+ /*
+ * A negative zero shall be turned into a positive one, because the
two do not have the same
+ * bit pattern and would therefore compare as different values.
`assertEquals(double,double)`
+ * compares the bit patterns, so it does detect a negative zero here.
+ */
+ assertEquals(0.0, azimuth(Bearing.ofAzimuth(deg(-0.0))), "The azimuth
shall not be a negative zero.");
+ }
+
+ /**
+ * Test that an altitude outside the range of −90 to +90 degrees is
rejected.
+ */
+ @Test
+ public void testAltitudeOutOfRange() {
+ assertThrows(IllegalArgumentException.class, () ->
Bearing.ofAzimuth(deg(0), deg( 91)));
+ assertThrows(IllegalArgumentException.class, () ->
Bearing.ofAzimuth(deg(0), deg(-91)));
+ assertThrows(IllegalArgumentException.class, () ->
Bearing.ofAzimuth(deg(0), deg(Double.NaN)));
+ assertThrows(IllegalArgumentException.class, () ->
Bearing.ofAzimuth(deg(Double.POSITIVE_INFINITY)));
+ }
+
+ /**
+ * Test that a direction which gives no usable direction is rejected.
+ */
+ @Test
+ public void testInvalidDirection() {
+ assertThrows(IllegalArgumentException.class, () ->
Bearing.ofDirection(vector(0, 0)),
+ "A vector of length zero gives no direction.");
+ assertThrows(IllegalArgumentException.class, () ->
Bearing.ofDirection(vector(0, 0, 0)));
+ assertThrows(IllegalArgumentException.class, () ->
Bearing.ofDirection(vector(1, Double.NaN)));
+ assertThrows(IllegalArgumentException.class, () ->
Bearing.ofDirection(vector(Double.POSITIVE_INFINITY, 1)));
+ }
+
+ /**
+ * Test that only the direction of the given vector is retained, not its
length.
+ */
+ @Test
+ public void testDirectionIsNormalized() {
+ final Bearing bearing = Bearing.ofDirection(vector(0, 5));
+ assertEquals(0, azimuth(bearing), TOLERANCE);
+ assertArrayEquals(new double[] {0, 1},
bearing.getDirection().toArrayDouble(), TOLERANCE,
+ "The direction shall be a unit vector.");
+ }
+
+ /**
+ * Test of a direction whose dimension allows no angle to be derived.
+ */
+ @Test
+ public void testNonDerivableDimension() {
+ for (final Vector<?> v : new Vector<?>[] {vector(3), vector(1, 2, 3,
4)}) {
+ final Bearing bearing = Bearing.ofDirection(v);
+ assertNull(bearing.getAzimuth(), "No azimuth can be derived in
this dimension.");
+ assertNull(bearing.getAltitude(), "No altitude can be derived in
this dimension.");
+ assertNotNull(bearing.getDirection());
+ assertEquals(1, bearing.getDirection().length(), TOLERANCE);
+ }
+ }
+
+ /**
+ * Test that the angles are converted to degrees when the bearing is
created.
+ */
+ @Test
+ public void testUnitConversion() {
+ final Bearing inRadians =
Bearing.ofAzimuth(Quantities.create(Math.PI/2, Units.RADIAN));
+ assertEquals(Units.DEGREE, inRadians.getAzimuth().getUnit(),
+ "Angles shall be canonicalized to degrees when the
bearing is created.");
+ assertEquals(90, azimuth(inRadians), TOLERANCE);
+ /*
+ * Equality is exact, and the conversion from radians to degrees is
not, so two bearings
+ * given in different units are not required to be equal. Only the
unit of the result and
+ * its value are pinned here.
+ */
+ assertEquals(Units.DEGREE,
Bearing.ofAzimuth(deg(90)).getAzimuth().getUnit());
+ }
+
+ /**
+ * Test that a bearing whose reference direction leads back to itself is
rejected.
+ */
+ @Test
+ public void testCyclicReference() {
+ assertThrows(IllegalArgumentException.class,
+ () -> Bearing.ofAzimuth(deg(0), null, new Stub(null, true),
Rotation.CLOCKWISE),
+ "A reference direction referring to itself shall be
rejected.");
+
+ final Stub a = new Stub(null, false);
+ final Stub b = new Stub(a, false);
+ a.reference = b;
+ assertThrows(IllegalArgumentException.class,
+ () -> Bearing.ofAzimuth(deg(0), null, a, Rotation.CLOCKWISE),
+ "A cycle between two reference directions shall be rejected.");
+
+ assertThrows(IllegalArgumentException.class,
+ () -> Bearing.ofAzimuth(deg(0), null, new Endless(),
Rotation.CLOCKWISE),
+ "An endless chain of reference directions shall be rejected,
not walked forever.");
+ }
+
+ /**
+ * Test that a finite chain of reference directions is accepted.
+ */
+ @Test
+ public void testNestedReference() {
+ final Bearing inner = Bearing.ofAzimuth(deg(20));
+ final Bearing outer = Bearing.ofAzimuth(deg(10), null, inner,
Rotation.CLOCKWISE);
+ final Bearing nested = Bearing.ofAzimuth(deg(5), null, outer,
Rotation.CLOCKWISE);
+ assertSame(inner, outer.getReference());
+ assertSame(outer, nested.getReference());
+ }
+
+ /**
+ * Test of {@code equals(Object)} and {@code hashCode()}.
+ */
+ @Test
+ public void testEqualsAndHashCode() {
+ final Bearing bearing = Bearing.ofAzimuth(deg(45), deg(10));
+ assertEquals(bearing, bearing);
+ assertNotEquals(bearing, null);
+
+ final Bearing same = Bearing.ofAzimuth(deg(45), deg(10));
+ assertEquals(bearing, same);
+ assertEquals(bearing.hashCode(), same.hashCode());
+
+ assertNotEquals(bearing, Bearing.ofAzimuth(deg(46), deg(10)));
+ assertNotEquals(bearing, Bearing.ofAzimuth(deg(45), deg(11)));
+ assertNotEquals(bearing, Bearing.ofAzimuth(deg(45), deg(10),
FixedDirection.MAGNETIC_NORTH, Rotation.CLOCKWISE));
+ assertNotEquals(bearing, Bearing.ofAzimuth(deg(45), deg(10),
FixedDirection.TRUE_NORTH, Rotation.COUNTER_CLOCKWISE));
+ /*
+ * An implementation other than the one of this module is never equal,
so that equality
+ * stays symmetric.
+ */
+ assertNotEquals(bearing, new Stub(FixedDirection.TRUE_NORTH, false));
+ }
+
+ /**
+ * Test that a bearing shares no mutable state with its argument or with
its result.
+ */
+ @Test
+ public void testDefensiveCopy() {
+ final Vector<?> source = vector(0, 1);
+ final Bearing bearing = Bearing.ofDirection(source);
+ source.set(0, 100);
+ assertArrayEquals(new double[] {0, 1},
bearing.getDirection().toArrayDouble(), TOLERANCE,
+ "Modifying the given vector shall not modify the
bearing.");
+
+ final Vector<?> result = bearing.getDirection();
+ result.set(0, 100);
+ assertArrayEquals(new double[] {0, 1},
bearing.getDirection().toArrayDouble(), TOLERANCE,
+ "Modifying the returned vector shall not modify the
bearing.");
+ }
+
+ /**
+ * Test that the azimuth does not depend on the order in which the axes
are given.
+ */
+ @Test
+ public void testAxisOrder() {
+ /*
+ * `normalizedGeographic()` gives the coordinates in the (longitude,
latitude) order, which
+ * is (east, north), while `geographic()` gives them in the (latitude,
longitude) order,
+ * which is (north, east). The same physical direction shall give the
same azimuth.
+ */
+ final CoordinateReferenceSystem eastNorth =
CommonCRS.WGS84.normalizedGeographic();
+ final CoordinateReferenceSystem northEast =
CommonCRS.WGS84.geographic();
+ assertEquals(90, azimuth(Bearing.ofDirection(vector(eastNorth, 1,
0))), TOLERANCE, "Toward east.");
+ assertEquals(90, azimuth(Bearing.ofDirection(vector(northEast, 0,
1))), TOLERANCE, "Toward east.");
+ assertEquals( 0, azimuth(Bearing.ofDirection(vector(eastNorth, 0,
1))), TOLERANCE, "Toward north.");
+ assertEquals( 0, azimuth(Bearing.ofDirection(vector(northEast, 1,
0))), TOLERANCE, "Toward north.");
+ }
+
+ /**
+ * Test of {@code toString()}.
+ */
+ @Test
+ public void testToString() {
+ final String text = Bearing.ofAzimuth(deg(45), deg(10)).toString();
+ assertTrue(text.contains("45"), text);
+ assertTrue(text.contains("10"), text);
+ assertTrue(text.contains("TRUE_NORTH"), text);
+ assertTrue(text.contains("CLOCKWISE"), text);
+ }
+
+ /**
+ * A bearing which gives an arbitrary reference direction, for testing the
constraint that
+ * the reference direction of a bearing shall not refer to that bearing
transitively.
+ */
+ private static final class Stub implements Bearing {
+ /** The reference direction to return, or {@code null} for {@code
this}. */
+ ReferenceDirection reference;
+
+ /** Whether {@link #getReference()} shall return {@code this}. */
+ private final boolean self;
+
+ /** Creates a new stub returning the given reference direction. */
+ Stub(final ReferenceDirection reference, final boolean self) {
+ this.reference = reference;
+ this.self = self;
+ }
+
+ @Override public Quantity<Angle> getAzimuth() {return null;}
+ @Override public Quantity<Angle> getAltitude() {return null;}
+ @Override public Vector<?> getDirection() {return null;}
+ @Override public Rotation getRotation() {return
Rotation.CLOCKWISE;}
+ @Override public ReferenceDirection getReference() {return self ? this
: reference;}
+ }
+
+ /**
+ * A bearing returning a new reference direction on each call, so that the
chain of reference
+ * directions is endless without ever containing twice the same instance.
+ */
+ private static final class Endless implements Bearing {
+ /** Creates a new stub. */
+ Endless() {
+ }
+
+ @Override public Quantity<Angle> getAzimuth() {return null;}
+ @Override public Quantity<Angle> getAltitude() {return null;}
+ @Override public Vector<?> getDirection() {return null;}
+ @Override public Rotation getRotation() {return
Rotation.CLOCKWISE;}
+ @Override public ReferenceDirection getReference() {return new
Endless();}
+ }
+}