blob: 676943c2dbe41509d07a35189e5a8ac764dee531 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.spherical.twod;
import java.util.Objects;
import org.apache.commons.geometry.core.Geometry;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.exception.GeometryException;
import org.apache.commons.geometry.core.internal.Equivalency;
import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.spherical.oned.AngularInterval;
import org.apache.commons.geometry.spherical.oned.Point1S;
/** Class representing a great circle on the 2-sphere. A great circle is the
* intersection of a sphere with a plane that passes through its center. It is
* the largest diameter circle that can be drawn on the sphere and partitions the
* sphere into two hemispheres. The vectors {@code u} and {@code v} lie in the great
* circle plane, while the vector {@code w} (the pole) is perpendicular to it.
*
* <p>Instances of this class are guaranteed to be immutable.</p>
*/
public final class GreatCircle extends AbstractHyperplane<Point2S>
implements EmbeddingHyperplane<Point2S, Point1S>, Equivalency<GreatCircle> {
/** Pole or circle center. */
private final Vector3D.Unit pole;
/** First axis in the equator plane, origin of the azimuth angles. */
private final Vector3D.Unit u;
/** Second axis in the equator plane, in quadrature with respect to u. */
private final Vector3D.Unit v;
/** Simple constructor. Callers are responsible for ensuring the inputs are valid.
* @param pole pole vector of the great circle
* @param u u axis in the equator plane
* @param v v axis in the equator plane
* @param precision precision context used for floating point comparisons
*/
private GreatCircle(final Vector3D.Unit pole, final Vector3D.Unit u, final Vector3D.Unit v,
final DoublePrecisionContext precision) {
super(precision);
this.pole = pole;
this.u = u;
this.v = v;
}
/** Get the pole of the great circle. This vector is perpendicular to the
* equator plane of the instance.
* @return pole of the great circle
*/
public Vector3D.Unit getPole() {
return pole;
}
/** Get the spherical point located at the positive pole of the instance.
* @return the spherical point located at the positive pole of the instance
*/
public Point2S getPolePoint() {
return Point2S.from(pole);
}
/** Get the u axis of the great circle. This vector is located in the equator plane and defines
* the {@code 0pi} location of the embedded subspace.
* @return u axis of the great circle
*/
public Vector3D.Unit getU() {
return u;
}
/** Get the v axis of the great circle. This vector lies in the equator plane,
* perpendicular to the u-axis.
* @return v axis of the great circle
*/
public Vector3D.Unit getV() {
return v;
}
/** Get the w (pole) axis of the great circle. The method is equivalent to {@code #getPole()}.
* @return the w (pole) axis of the great circle.
* @see #getPole()
*/
public Vector3D.Unit getW() {
return getPole();
}
/** {@inheritDoc}
*
* <p>The returned offset values are in the range {@code [-pi/2, +pi/2]},
* with a point directly on the circle's pole vector having an offset of
* {@code -pi/2} and its antipodal point having an offset of {@code +pi/2}.
* Thus, the circle's pole vector points toward the <em>minus</em> side of
* the hyperplane.</p>
*
* @see #offset(Vector3D)
*/
@Override
public double offset(final Point2S point) {
return offset(point.getVector());
}
/** Get the offset (oriented distance) of a direction.
*
* <p>The offset computed here is equal to the angle between the circle's
* pole and the given vector minus {@code pi/2}. Thus, the pole vector
* has an offset of {@code -pi/2}, a point on the circle itself has an
* offset of {@code 0}, and the negation of the pole vector has an offset
* of {@code +pi/2}.</p>
* @param vec vector to compute the offset for
* @return the offset (oriented distance) of a direction
*/
public double offset(final Vector3D vec) {
return pole.angle(vec) - Geometry.HALF_PI;
}
/** Get the azimuth angle of a point relative to this great circle instance,
* in the range {@code [0, 2pi)}.
* @param pt point to compute the azimuth for
* @return azimuth angle of the point in the range {@code [0, 2pi)}
*/
public double azimuth(final Point2S pt) {
return azimuth(pt.getVector());
}
/** Get the azimuth angle of a vector in the range {@code [0, 2pi)}.
* The azimuth angle is the angle of the projection of the argument on the
* equator plane relative to the plane's u-axis. Since the vector is
* projected onto the equator plane, it does not need to belong to the circle.
* Vectors parallel to the great circle's pole do not have a defined azimuth angle.
* In these cases, the method follows the rules of the
* {@code Math#atan2(double, double)} method and returns {@code 0}.
* @param vector vector to compute the great circle azimuth of
* @return azimuth angle of the vector around the great circle in the range
* {@code [0, 2pi)}
* @see #toSubspace(Point2S)
*/
public double azimuth(final Vector3D vector) {
double az = Math.atan2(vector.dot(v), vector.dot(u));
// adjust range
if (az < 0) {
az += Geometry.TWO_PI;
}
return az;
}
/** Get the vector on the great circle with the given azimuth angle.
* @param azimuth azimuth angle in radians
* @return the point on the great circle with the given phase angle
*/
public Vector3D vectorAt(final double azimuth) {
return Vector3D.linearCombination(Math.cos(azimuth), u, Math.sin(azimuth), v);
}
/** {@inheritDoc} */
@Override
public Point2S project(final Point2S point) {
final double az = azimuth(point.getVector());
return Point2S.from(vectorAt(az));
}
/** {@inheritDoc}
*
* <p>The returned instance has the same u-axis but opposite pole and v-axis
* as this instance.</p>
*/
@Override
public GreatCircle reverse() {
return new GreatCircle(pole.negate(), u, v.negate(), getPrecision());
}
/** {@inheritDoc} */
@Override
public GreatCircle transform(final Transform<Point2S> transform) {
final Point2S tu = transform.apply(Point2S.from(u));
final Point2S tv = transform.apply(Point2S.from(v));
return fromPoints(tu, tv, getPrecision());
}
/** {@inheritDoc} */
@Override
public boolean similarOrientation(final Hyperplane<Point2S> other) {
final GreatCircle otherCircle = (GreatCircle) other;
return pole.dot(otherCircle.pole) > 0.0;
}
/** {@inheritDoc} */
@Override
public GreatArc span() {
return GreatArc.fromInterval(this, AngularInterval.full());
}
/** Create an arc on this circle between the given points.
* @param start start point
* @param end end point
* @return an arc on this circle between the given points
* @throws GeometryException if the specified interval is not
* convex (ie, the angle between the points is greater than {@code pi}
*/
public GreatArc arc(final Point2S start, final Point2S end) {
return arc(toSubspace(start), toSubspace(end));
}
/** Create an arc on this circle between the given subspace points.
* @param start start subspace point
* @param end end subspace point
* @return an arc on this circle between the given subspace points
* @throws GeometryException if the specified interval is not
* convex (ie, the angle between the points is greater than {@code pi}
*/
public GreatArc arc(final Point1S start, final Point1S end) {
return arc(start.getAzimuth(), end.getAzimuth());
}
/** Create an arc on this circle between the given subspace azimuth values.
* @param start start subspace azimuth
* @param end end subspace azimuth
* @return an arc on this circle between the given subspace azimuths
* @throws GeometryException if the specified interval is not
* convex (ie, the angle between the points is greater than {@code pi}
*/
public GreatArc arc(final double start, final double end) {
return arc(AngularInterval.Convex.of(start, end, getPrecision()));
}
/** Create an arc on this circle consisting of the given subspace interval.
* @param interval subspace interval
* @return an arc on this circle consisting of the given subspace interval
*/
public GreatArc arc(final AngularInterval.Convex interval) {
return GreatArc.fromInterval(this, interval);
}
/** Return one of the two intersection points between this instance and the argument.
* If the circles occupy the same space (ie, their poles are parallel or anti-parallel),
* then null is returned. Otherwise, the intersection located at the cross product of
* the pole of this instance and that of the argument is returned (ie, {@code thisPole.cross(otherPole)}.
* The other intersection point of the pair is antipodal to this point.
* @param other circle to intersect with
* @return one of the two intersection points between this instance and the argument
*/
public Point2S intersection(final GreatCircle other) {
final Vector3D cross = pole.cross(other.pole);
if (!cross.eq(Vector3D.ZERO, getPrecision())) {
return Point2S.from(cross);
}
return null;
}
/** Compute the angle between this great circle and the argument.
* The return value is the angle between the poles of the two circles,
* in the range {@code [0, pi]}.
* @param other great circle to compute the angle with
* @return the angle between this great circle and the argument in the
* range {@code [0, pi]}
* @see #angle(GreatCircle, Point2S)
*/
public double angle(final GreatCircle other) {
return pole.angle(other.pole);
}
/** Compute the angle between this great circle and the argument, measured
* at the intersection point closest to the given point. The value is computed
* as if a tangent line was drawn from each great circle at the intersection
* point closest to {@code pt}, and the angle required to rotate the tangent
* line representing the current instance to align with that of the given
* instance was measured. The return value lies in the range {@code [-pi, pi)} and
* has an absolute value equal to that returned by {@link #angle(GreatCircle)}, but
* possibly a different sign. If the given point is equidistant from both intersection
* points (as evaluated by this instance's precision context), then the point is assumed
* to be closest to the point opposite the cross product of the two poles.
* @param other great circle to compute the angle with
* @param pt point determining the circle intersection to compute the angle at
* @return the angle between this great circle and the argument as measured at the
* intersection point closest to the given point; the value is in the range
* {@code [-pi, pi)}
* @see #angle(GreatCircle)
*/
public double angle(final GreatCircle other, final Point2S pt) {
final double theta = angle(other);
final Vector3D cross = pole.cross(other.pole);
return getPrecision().gt(pt.getVector().dot(cross), 0) ?
theta :
-theta;
}
/** {@inheritDoc} */
@Override
public Point1S toSubspace(final Point2S point) {
return Point1S.of(azimuth(point.getVector()));
}
/** {@inheritDoc} */
@Override
public Point2S toSpace(final Point1S point) {
return Point2S.from(vectorAt(point.getAzimuth()));
}
/** {@inheritDoc} */
@Override
public boolean eq(final GreatCircle other) {
if (this == other) {
return true;
}
final DoublePrecisionContext precision = getPrecision();
return precision.equals(other.getPrecision()) &&
pole.eq(other.pole, precision) &&
u.eq(other.u, precision) &&
v.eq(other.v, precision);
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return Objects.hash(pole, u, v, getPrecision());
}
/** {@inheritDoc} */
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof GreatCircle)) {
return false;
}
GreatCircle other = (GreatCircle) obj;
return Objects.equals(this.pole, other.pole) &&
Objects.equals(this.u, other.u) &&
Objects.equals(this.v, other.v) &&
Objects.equals(this.getPrecision(), other.getPrecision());
}
/** {@inheritDoc} */
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName())
.append("[pole= ")
.append(pole)
.append(", u= ")
.append(u)
.append(", v= ")
.append(v)
.append(']');
return sb.toString();
}
/** Create a great circle instance from its pole vector. An arbitrary u-axis is chosen.
* @param pole pole vector for the great circle
* @param precision precision context used to compare floating point values
* @return a great circle defined by the given pole vector
*/
public static GreatCircle fromPole(final Vector3D pole, final DoublePrecisionContext precision) {
final Vector3D.Unit u = pole.orthogonal();
final Vector3D.Unit v = pole.cross(u).normalize();
return new GreatCircle(pole.normalize(), u, v, precision);
}
/** Create a great circle instance from its pole vector and a vector representing the u-axis
* in the equator plane. The u-axis vector defines the {@code 0pi} location for the embedded
* subspace.
* @param pole pole vector for the great circle
* @param u u-axis direction for the equator plane
* @param precision precision context used to compare floating point values
* @return a great circle defined by the given pole vector and u-axis direction
*/
public static GreatCircle fromPoleAndU(final Vector3D pole, final Vector3D u,
final DoublePrecisionContext precision) {
final Vector3D.Unit unitPole = pole.normalize();
final Vector3D.Unit unitX = pole.orthogonal(u);
final Vector3D.Unit unitY = pole.cross(u).normalize();
return new GreatCircle(unitPole, unitX, unitY, precision);
}
/** Create a great circle instance from two points on the circle. The u-axis of the
* instance points to the location of the first point. The orientation of the circle
* is along the shortest path between the two points.
* @param a first point on the great circle
* @param b second point on the great circle
* @param precision precision context used to compare floating point values
* @return great circle instance containing the given points
* @throws GeometryException if either of the given points is NaN or infinite, or if the given points are
* equal or antipodal as evaluated by the given precision context
*/
public static GreatCircle fromPoints(final Point2S a, final Point2S b,
final DoublePrecisionContext precision) {
if (!a.isFinite() || !b.isFinite()) {
throw new GeometryException("Invalid points for great circle: " + a + ", " + b);
}
String err = null;
final double dist = a.distance(b);
if (precision.eqZero(dist)) {
err = "equal";
} else if (precision.eq(dist, Geometry.PI)) {
err = "antipodal";
}
if (err != null) {
throw new GeometryException("Cannot create great circle from points " + a + " and " + b +
": points are " + err);
}
final Vector3D.Unit u = a.getVector().normalize();
final Vector3D.Unit pole = u.cross(b.getVector()).normalize();
final Vector3D.Unit v = pole.cross(u).normalize();
return new GreatCircle(pole, u, v, precision);
}
}