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();}
+    }
+}

Reply via email to