blob: 36c6e614d471cd004906831ebfe1477db114f133 [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.sis.measure;
import java.util.Locale;
import java.util.Formatter;
import java.util.Formattable;
import java.util.FormattableFlags;
import java.text.Format;
import java.text.ParseException;
import java.io.Serializable;
import javax.measure.Unit;
import javax.measure.IncommensurableException;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.internal.util.Strings;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.Classes;
import static java.lang.Double.doubleToLongBits;
import static org.apache.sis.math.MathFunctions.isNegative;
/**
* An angle in decimal degrees. An angle is the amount of rotation needed to bring one line or plane
* into coincidence with another. Various kind of angles are used in geographic information systems,
* some of them having a specialized class in Apache SIS:
*
* <ul>
* <li>{@linkplain Latitude} is an angle ranging from 0° at the equator to 90° at the poles.</li>
* <li>{@linkplain Longitude} is an angle measured east-west from a prime meridian (usually Greenwich, but not necessarily).</li>
* <li><cite>Azimuth</cite> is a direction given by an angle between 0° and 360° measured clockwise from North.</li>
* <li><cite>Bearing</cite> is a direction given by an angle between 0° and 90° in a quadrant defined by a cardinal direction.</li>
* <li><cite>Bearing</cite> is also sometime used in navigation for an angle relative to the vessel forward direction.</li>
* <li><cite>Deflection angle</cite> is the angle between a line and the prolongation of a preceding line.</li>
* <li><cite>Interior angle</cite> is an angle measured between two lines of sight.</li>
* <li>{@linkplain ElevationAngle Elevation angle} is the angular height from the horizontal plane to an object above the horizon.</li>
* </ul>
*
* <h2>Formatting angles</h2>
* The recommended way to format angles is to instantiate an {@link AngleFormat} once, then to
* reuse it many times. As a convenience, {@code Angle} objects can also be formatted by the
* {@code "%s"} conversion specifier of {@link Formatter}, but this is less efficient for this
* class.
*
* <h2>Immutability and thread safety</h2>
* This class and the {@link Latitude} / {@link Longitude} subclasses are immutable, and thus
* inherently thread-safe. Other subclasses may or may not be immutable, at implementation choice
* (see {@link java.lang.Number} for an example of a similar in purpose class having mutable subclasses).
*
* @author Martin Desruisseaux (MPO, IRD, Geomatys)
* @version 0.8
*
* @see Latitude
* @see Longitude
* @see AngleFormat
*
* @since 0.3
* @module
*/
public class Angle implements Comparable<Angle>, Formattable, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = 3701568577051191744L;
/**
* A shared instance of {@link AngleFormat}.
*
* @see #getAngleFormat()
*/
private static Format format;
/**
* Angle value in decimal degrees. We use decimal degrees as the storage unit
* instead than radians in order to avoid rounding errors, since there is no
* way to represent 30°, 45°, 90°, 180°, <i>etc.</i> in radians without errors.
*/
private final double θ;
/**
* Constructs a new angle with the specified value in decimal degrees.
*
* @param θ angle in decimal degrees.
*/
public Angle(final double θ) {
this = θ;
}
/**
* Constructs a newly allocated {@code Angle} object that contain the angular value
* represented by the string. The string should represent an angle in either fractional
* degrees (e.g. 45.5°) or degrees with minutes and seconds (e.g. 45°30').
*
* <p>This is a convenience constructor mostly for testing purpose, since it uses a fixed
* locale. Developers should consider using {@link AngleFormat} for end-user applications
* instead than this constructor.</p>
*
* @param text a string to be converted to an {@code Angle}.
* @throws NumberFormatException if the string does not contain a parsable angle.
*
* @see AngleFormat#parse(String)
*/
public Angle(final String text) throws NumberFormatException {
final Object angle;
try {
synchronized (Angle.class) {
angle = getAngleFormat().parseObject(text);
}
} catch (ParseException exception) {
/*
* Use Exception.getMessage() instead than getLocalizedMessage() because the later
* is formatted in the AngleFormat locale, which is hard-coded to Locale.ROOT in our
* 'getAngleFormat()' implementation. The getMessage() method uses the system locale,
* which is what we actually want.
*/
throw (NumberFormatException) new NumberFormatException(exception.getMessage()).initCause(exception);
}
final Class<?> type = angle.getClass();
if (type == Angle.class || getClass().isAssignableFrom(type)) {
this = ((Angle) angle).θ;
} else {
throw new NumberFormatException(text);
}
}
/**
* Returns the angular value of the axis having the given direction.
* This helper method is used for subclass constructors expecting a {@link DirectPosition} argument.
*
* @param position the position from which to get an angular value.
* @param positive axis direction of positive values.
* @param negative axis direction of negative values.
* @return angular value in degrees.
* @throws IllegalArgumentException if the given coordinate it not associated to a CRS,
* or if no axis oriented toward the given directions is found, or if that axis
* does not use {@linkplain Units#isAngular angular units}.
*/
static double valueOf(final DirectPosition position, final AxisDirection positive, final AxisDirection negative) {
final CoordinateReferenceSystem crs = position.getCoordinateReferenceSystem();
if (crs == null) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.UnspecifiedCRS));
}
final CoordinateSystem cs = crs.getCoordinateSystem();
final int dimension = cs.getDimension();
IncommensurableException cause = null;
for (int i=0; i<dimension; i++) {
final CoordinateSystemAxis axis = cs.getAxis(i);
final AxisDirection dir = axis.getDirection();
final boolean isPositive = dir.equals(positive);
if (isPositive || dir.equals(negative)) {
double value = position.getOrdinate(i);
if (!isPositive) value = -value;
final Unit<?> unit = axis.getUnit();
if (unit != Units.DEGREE) try {
value = unit.getConverterToAny(Units.DEGREE).convert(value);
} catch (IncommensurableException e) {
cause = e;
break;
}
return value;
}
}
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalCRSType_1,
Classes.getLeafInterfaces(crs.getClass(), CoordinateReferenceSystem.class)[0]), cause);
}
/**
* Returns the angle value in decimal degrees.
*
* @return the angle value in decimal degrees.
*/
public double degrees() {
return θ;
}
/**
* Returns the angle value in radians.
*
* @return the angle value in radians.
*/
public double radians() {
return Math.toRadians(θ);
}
/**
* Returns a hash code for this {@code Angle} object.
*/
@Override
public int hashCode() {
final long code = Double.doubleToLongBits(θ);
return (int) code ^ (int) (code >>> 32) ^ (int) serialVersionUID;
}
/**
* Compares the specified object with this angle for equality.
*
* @param object the object to compare with this angle for equality.
* @return {@code true} if the given object is equal to this angle.
*/
@Override
public boolean equals(final Object object) {
if (object == this) {
return true;
}
if (object != null && getClass() == object.getClass()) {
return doubleToLongBits(θ) == doubleToLongBits(((Angle) object).θ);
}
return false;
}
/**
* Compares two {@code Angle} objects numerically. The comparison
* is done as if by the {@link Double#compare(double, double)} method.
*
* @param that the angle to compare with this object for order.
* @return -1 if this angle is smaller than the given one, +1 if greater or 0 if equals.
*/
@Override
public int compareTo(final Angle that) {
return Double.compare(this.θ, that.θ);
}
/**
* Upper threshold before to format an angle as an ordinary number.
* This is set to 90° in the case of latitude numbers.
*/
double maximum() {
return 360;
}
/**
* Returns the hemisphere character for an angle of the given sign.
* This is used only by {@link #toString()}, not by {@link AngleFormat}.
*/
char hemisphere(final boolean negative) {
return 0;
}
/**
* Returns a string representation of this {@code Angle} object.
* This is a convenience method mostly for debugging purpose, since it uses a fixed locale.
* Developers should consider using {@link AngleFormat} for end-user applications instead
* than this method.
*
* @see AngleFormat#format(double)
*/
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
double m = Math.abs(θ);
final boolean isSmall = m <= (1 / 3600E+3); // 1E-3 arc-second.
if ((isSmall || m > maximum()) && m != 0) {
final char h = hemisphere(isNegative(θ));
if (h == 0) {
m = θ; // Restore the sign.
}
char symbol = '°';
if (isSmall) {
symbol = '″';
m *= 3600;
}
buffer.append(m).append(symbol);
if (h != 0) {
buffer.append(h);
}
} else {
synchronized (Angle.class) {
buffer = getAngleFormat().format(this, buffer, null);
}
}
return buffer.toString();
}
/**
* Returns a shared instance of {@link AngleFormat}. The return type is
* {@link Format} in order to avoid class loading before necessary.
*
* <p>This method must be invoked in a {@code synchronized(Angle.class)} block. We use
* synchronization instead than static class initialization because {@code AngleFormat}
* is not thread-safe, so it needs to be used in a synchronized block anyway. We could
* avoid synchronization by using {@link ThreadLocal}, but this brings other issues in
* OSGi context. Given that our Javadoc said that {@link #Angle(String)} and {@link #toString()}
* should be used mostly for debugging purpose, we consider not worth to ensure high
* concurrency capability here.</p>
*/
private static Format getAngleFormat() {
assert Thread.holdsLock(Angle.class);
if (format == null) {
format = new AngleFormat(Locale.ROOT);
}
return format;
}
/**
* Formats this angle using the provider formatter. This method is invoked when an
* {@code Angle} object is formatted using the {@code "%s"} conversion specifier of
* {@link Formatter}. Users don't need to invoke this method explicitly.
*
* <p>Special cases:</p>
* <ul>
* <li>If the precision is 0, then this method formats an empty string.</li>
* <li>If the precision is 1 and this angle is a {@link Latitude} or {@link Longitude},
* then this method formats only the hemisphere symbol.</li>
* <li>Otherwise the precision, if positive, is given to {@link AngleFormat#setMaximumWidth(int)}.</li>
* </ul>
*
* @param formatter the formatter in which to format this angle.
* @param flags {@link FormattableFlags#LEFT_JUSTIFY} for left alignment, or 0 for right alignment.
* @param width minimal number of characters to write, padding with {@code ' '} if necessary.
* @param precision maximal number of characters to write, or -1 if no limit.
*/
@Override
public void formatTo(final Formatter formatter, final int flags, final int width, final int precision) {
final String value;
if (precision == 0) {
value = "";
} else {
final char h;
int w = precision; // To be decremented only if we may truncate and an hemisphere symbol exist.
if (w > 0 && (h = hemisphere(isNegative(θ))) != 0 && --w == 0) {
value = Character.toString(h);
} else {
final AngleFormat format = new AngleFormat(formatter.locale());
if (w > 0) {
format.setMaximumWidth(w);
}
value = format.format(this, new StringBuffer(), null).toString();
}
}
Strings.formatTo(formatter, flags, width, precision, value);
}
}