
org.openstreetmap.atlas.utilities.scalars.Angle Maven / Gradle / Ivy
package org.openstreetmap.atlas.utilities.scalars;
import java.io.Serializable;
import org.openstreetmap.atlas.geography.Heading;
/**
* Angle, between -180 degrees (included) and 180 degrees (excluded). Precision is 10 microdegrees,
* or one ten millionth of a degree. The term dm7 denotes a unit of one ten millionth of a degree.
* Angle's degree circle starts from -180 and increases in the clock-wise direction towards 0 and
* then it goes to 180 (excluded).
*
* @author matthieun
* @author mkalender
*/
public class Angle implements Serializable
{
// There are ten million microdegrees per degree
public static final int DM7_PER_DEGREE = 10_000_000;
// An angle is >= -180 degrees
public static final int MINIMUM_DM7 = -1_800_000_000;
// An angle is < 180 degrees
public static final int MAXIMUM_DM7 = 1_800_000_000;
// There are approximately 57 degrees per radian
public static final int DM7_PER_RADIAN = 572_957_795;
// When precision is needed
public static final double DM7_PER_RADIAN_DOUBLE = Double.valueOf(MAXIMUM_DM7) / Math.PI;
// This difference does not fit in an int!
public static final long REVOLUTION_DM7 = (long) MAXIMUM_DM7 - (long) MINIMUM_DM7;
// Useful Angle constants
public static final Angle MINIMUM = Angle.dm7(MINIMUM_DM7);
public static final Angle NONE = Angle.dm7(0L);
public static final Angle MAXIMUM = Angle.dm7(MAXIMUM_DM7 - 1L);
// dm7 unit per microdegree
public static final int DM7_PER_MICRODEGREE = 10;
// Threshold to print a dm7 value
public static final int DM7_PRINT_THRESHOLD = 10_000;
private static final long serialVersionUID = -5120437813288084229L;
// The primitive store. It will always fit between MINIMUM_DM7 and MAXIMUM_DM7, so int is enough
private final int dm7;
/**
* Create an Angle from an angle value. Any value outside of [-180,180[ degrees will be
* translated to its equivalent within this range
*
* @param degrees
* The angle value in degrees
* @return The Angle object corresponding to this value in degrees.
*/
public static Angle degrees(final double degrees)
{
return dm7(Math.round(degrees * DM7_PER_DEGREE));
}
/**
* Create an Angle from an angle value. Any value outside of [-180,180[ degrees will be
* translated to its equivalent within this range
*
* @param dm7
* The angle value in dm7
* @return The Angle object corresponding to this value in dm7.
*/
public static Angle dm7(final long dm7)
{
long rollingMicroDegrees = dm7 % REVOLUTION_DM7;
// Add a full 360 degrees until the number is within [-180,180[.
if (rollingMicroDegrees < MINIMUM_DM7)
{
rollingMicroDegrees += REVOLUTION_DM7;
}
// Subtract a full 360 degrees until the number is within [-180,180[.
if (rollingMicroDegrees >= MAXIMUM_DM7)
{
rollingMicroDegrees -= REVOLUTION_DM7;
}
// Store the angle as modulo 360 degrees
return new Angle((int) rollingMicroDegrees);
}
/**
* Create an Angle from an angle value. Any value outside of [-Pi,Pi[ degrees will be translated
* to its equivalent within this range
*
* @param radians
* The angle value in radians
* @return The Angle object corresponding to this value in radians.
*/
public static Angle radians(final double radians)
{
return dm7(Math.round(radians * DM7_PER_RADIAN));
}
/**
* Constructor.
*
* @param dm7
* An angle value in dm7, between MINIMUM_DM7 and MAXIMUM_DM7
*/
protected Angle(final int dm7)
{
this.dm7 = assertDm7(dm7);
}
/**
* Add another angle to this angle
*
* @param that
* The other {@link Angle} to add
* @return The angle representing the sum
*/
public final Angle add(final Angle that)
{
// The dm7 function takes care of maintaining the value inside the bounds.
return Angle.dm7(this.getDm7() + that.getDm7());
}
/**
* @return The value of this {@link Angle} in degrees.
*/
public double asDegrees()
{
return (double) this.asDm7() / DM7_PER_DEGREE;
}
/**
* @return The value of this {@link Angle} in one tenth of a microdegree. This returns a long
* instead of an int, because {@link Heading} (a sub-class of {@link Angle}) is based on
* 0-360 degrees angles that might not fit within int at the dm7 level.
*/
public long asDm7()
{
return this.dm7;
}
/**
* @return the {@link Angle} object with positive values. E.g. -100 degree will be 100 degree.
* Note -180 degree will return 179.9999999
*/
public Angle asPositiveAngle()
{
if (this.dm7 == MINIMUM_DM7)
{
return MAXIMUM;
}
return Angle.dm7(Math.abs(this.dm7));
}
/**
* @return The value of this {@link Angle} in positive radians.
*/
public double asPositiveRadians()
{
return asRadians() < 0 ? asRadians() + 2 * Math.PI : asRadians();
}
/**
* @return The value of this {@link Angle} in radians. Can be negative or positive.
*/
public double asRadians()
{
return this.asDm7() / DM7_PER_RADIAN_DOUBLE;
}
/**
* Returns the difference between two {@link Angle}s. Returned {@link Angle} will never be
* negative.
*
* @param that
* {@link Angle} to compare against
* @return Positive difference value between given {@link Angle}s
*/
public final Angle difference(final Angle that)
{
return this.subtract(that).asPositiveAngle();
}
@Override
public boolean equals(final Object other)
{
if (other instanceof Angle)
{
final Angle that = (Angle) other;
// This is specifically to make sure that the two angles are compared on their actual
// dm7 value, rather than what the asDm7() method returns. This is to allow angle
// sub-classes to have multiple ways to return the value of one single angle: example is
// Longitude, with two ways of representing the Angle -180 degrees, with -180 and +180.
// Another example is Heading, which is an Angle, but displays its values in [0, 360[
// degrees.
return this.getDm7() == that.getDm7();
}
return false;
}
@Override
public int hashCode()
{
return Long.hashCode(this.dm7);
}
/**
* Compares this {@link Angle} with another {@link Angle}.
*
* @param other
* Another {@link Angle} to compare against
* @return true if this {@link Angle} is greater than other {@link Angle}
*/
public final boolean isGreaterThan(final Angle other)
{
if (other.getClass() == this.getClass())
{
return this.asDm7() > other.asDm7();
}
return this.getDm7() > other.getDm7();
}
/**
* Compares this {@link Angle} with another {@link Angle}.
*
* @param other
* Another {@link Angle} to compare against
* @return true if this {@link Angle} is greater than or equal to other {@link Angle}
*/
public final boolean isGreaterThanOrEqualTo(final Angle other)
{
if (other.getClass() == this.getClass())
{
return this.asDm7() >= other.asDm7();
}
return this.getDm7() >= other.getDm7();
}
/**
* Compares this {@link Angle} with another {@link Angle}.
*
* @param other
* Another {@link Angle} to compare against
* @return true if this {@link Angle} is less than other {@link Angle}
*/
public final boolean isLessThan(final Angle other)
{
if (other.getClass() == this.getClass())
{
return this.asDm7() < other.asDm7();
}
return this.getDm7() < other.getDm7();
}
/**
* Compares this {@link Angle} with another {@link Angle}.
*
* @param other
* Another {@link Angle} to compare against
* @return true if this {@link Angle} is less than or equal to other {@link Angle}
*/
public final boolean isLessThanOrEqualTo(final Angle other)
{
if (other.getClass() == this.getClass())
{
return this.asDm7() <= other.asDm7();
}
return this.getDm7() <= other.getDm7();
}
/**
* @return The average distance on Earth's surface of this angle taken from the center of Earth.
*/
public Distance onEarth()
{
return Distance.AVERAGE_EARTH_RADIUS.scaleBy(this.asPositiveRadians());
}
public final Angle reverse()
{
return Angle.dm7(this.getDm7() - Angle.REVOLUTION_DM7 / 2);
}
/**
* Subtracts an {@link Angle} from this.
*
* @param that
* The angle to subtract
* @return The angle resulting from the subtraction
*/
public final Angle subtract(final Angle that)
{
// The dm7 function takes care of maintaining the value inside the bounds.
return Angle.dm7(this.getDm7() - that.getDm7());
}
@Override
public String toString()
{
if (Math.abs(this.getDm7()) >= DM7_PRINT_THRESHOLD)
{
return this.asDegrees() + " degrees";
}
return this.asDm7() + " tenths of microdegrees";
}
/**
* Resolving strategy for any invalid or out of bounds angle value. Sub-classes need to override
* this method to change the resolution strategy. The default strategy is throwing an exception
* if the dm7 value is smaller than -180 degrees or larger than or equal to 180 degrees.
*
* @param dm7
* The proposed dm7 value
* @return The corrected dm7 value
*/
protected int assertDm7(final int dm7)
{
if (dm7 < MINIMUM_DM7 || dm7 >= MAXIMUM_DM7)
{
throw new IllegalArgumentException("Angle dm7 value " + dm7 + " is invalid.");
}
return dm7;
}
/**
* @return dm7 value as is. Classes that extend {@link Angle} might override
* {@link Angle#asDm7()}, but {@link Angle#getDm7()}. Therefore, this method is used for
* comparisons and calculations.
*/
private long getDm7()
{
return this.dm7;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy