/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.referencing.cs;
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.opengis.referencing.cs.AxisDirection;
/**
* Parses {@linkplain AxisDirection axis direction} of the kind
* "South along 90 deg East". Those directions are
* used in the EPSG database for polar stereographic projections.
*
* @version $Id$
* @source $URL$
* @author Martin Desruisseaux
* @since 2.7.2
*/
public final class DirectionAlongMeridian implements Comparable, Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 1602711631943838328L;
/**
* For floating point comparaisons.
*/
static final double EPS = 1E-10;
/**
* A parser for EPSG axis names. Examples:
*
* "South along 180 deg",
* "South along 90 deg East"
*/
private static final Pattern EPSG = Pattern.compile(
"(\\p{Graph}+)\\s+along\\s+([\\-\\p{Digit}\\.]+)\\s*(deg|°)\\s*(\\p{Graph}+)?",
Pattern.CASE_INSENSITIVE);
/**
* The base directions we are interested in. Any direction not in
* this group will be rejected by our parser.
*/
private static final AxisDirection[] BASE_DIRECTIONS = new AxisDirection[] {
AxisDirection.NORTH,
AxisDirection.SOUTH,
AxisDirection.EAST,
AxisDirection.WEST
};
/**
* The direction. Will be created only when first needed.
*
* @see #getDirection
*/
private transient AxisDirection direction;
/**
* The base direction, which must be {@link AxisDirection#NORTH} or
* {@link AxisDirection#SOUTH}.
*/
public final AxisDirection baseDirection;
/**
* The meridian, in degrees.
*/
public final double meridian;
/**
* Creates a direction.
*/
private DirectionAlongMeridian(final AxisDirection baseDirection, final double meridian) {
this.baseDirection = baseDirection;
this.meridian = meridian;
}
/**
* Returns the dimension along meridian for the specified axis direction, or {@code null} if
* none.
*/
public static DirectionAlongMeridian parse(final AxisDirection direction) {
final DirectionAlongMeridian candidate = parse(direction.name());
if (candidate != null) {
candidate.direction = direction;
}
return candidate;
}
/**
* If the specified name is a direction along some specific meridian,
* returns information about that. Otherwise returns {@code null}.
*/
public static DirectionAlongMeridian parse(final String name) {
final Matcher m = EPSG.matcher(name);
if (!m.matches()) {
// Not the expected pattern.
return null;
}
String group = m.group(1);
final AxisDirection baseDirection = findDirection(BASE_DIRECTIONS, group);
if (baseDirection == null || !AxisDirection.NORTH.equals(baseDirection.absolute())) {
// We expected "North" or "South" direction.
return null;
}
group = m.group(2);
double meridian;
try {
meridian = Double.parseDouble(group);
} catch (NumberFormatException exception) {
// Not a legal axis direction. Just ignore the exception,
// since we are supposed to return 'null' in this situation.
return null;
}
if (!(meridian >= -180 && meridian <= 180)) {
// Meridian is NaN or is not in the valid range.
return null;
}
group = m.group(4);
if (group != null) {
final AxisDirection sign = findDirection(BASE_DIRECTIONS, group);
final AxisDirection abs = sign.absolute();
if (sign == null || !AxisDirection.EAST.equals(abs)) {
// We expected "East" or "West" direction.
return null;
}
if (sign != abs) {
meridian = -meridian;
}
}
return new DirectionAlongMeridian(baseDirection, meridian);
}
/**
* Searchs for the specified name in the specified set of directions.
*/
private static AxisDirection findDirection(final AxisDirection[] values, final String direction) {
for (int i=0; i
* Example: the angle from "North along 90 deg East" to
* "North along 0 deg is 90°.
*/
public double getAngle(final DirectionAlongMeridian other) {
if (!baseDirection.equals(other.baseDirection)) {
return Double.NaN;
}
/*
* We want the following pair of axis:
* (NORTH along 90°E, NORTH along 0°)
* to give a positive angle of 90°
*/
double angle = meridian - other.meridian;
/*
* Forces to the [-180° .. +180°] range.
*/
if (angle < -180) {
angle += 360;
} else if (angle > 180) {
angle -= 360;
}
/*
* Reverses the sign for axis oriented toward SOUTH,
* so a positive angle is a right-handed system.
*/
if (!baseDirection.equals(baseDirection.absolute())) {
angle = -angle;
}
return angle;
}
/**
* Compares this direction with the specified one for order. This method tries to reproduce
* the ordering used for the majority of coordinate systems in the EPSG database, i.e. the
* ordering of a right-handed coordinate system. Examples of ordered pairs that we should
* get (extracted from the EPSG database):
*
*
* North along 90 deg East, | North along 0 deg |
* North along 75 deg West, | North along 165 deg West |
* South along 90 deg West, | South along 0 deg |
* South along 180 deg, | South along 90 deg West |
* North along 130 deg West | North along 140 deg East |
*
*/
public int compareTo(final Object object) {
final DirectionAlongMeridian that = (DirectionAlongMeridian) object;
final int c = baseDirection.compareTo(that.baseDirection);
if (c != 0) {
return c;
}
final double angle = getAngle(that);
if (angle < 0) return +1; // Really the opposite sign.
if (angle > 0) return -1; // Really the opposite sign.
return 0;
}
/**
* Tests this object for equality with the specified one.
* This method is used mostly for assertions.
*/
@Override
public boolean equals(final Object object) {
if (object instanceof DirectionAlongMeridian) {
final DirectionAlongMeridian that = (DirectionAlongMeridian) object;
return baseDirection.equals(that.baseDirection) &&
Double.doubleToLongBits(meridian) == Double.doubleToLongBits(that.meridian);
}
return false;
}
/**
* Returns a hash code value, for consistency with {@link #equals}.
*/
@Override
public int hashCode() {
final long code = Double.doubleToLongBits(meridian);
return (int)serialVersionUID ^ (int)code ^ (int)(code >> 32) + 37*baseDirection.hashCode();
}
/**
* Returns a string representation of this direction, using a syntax matching the one used
* by EPSG. This string representation will be used for creating a new {@link AxisDirection}.
* The generated name should be identical to EPSG name, but we use the generated one anyway
* (rather than the one provided by EPSG) in order to make sure that we create a single
* {@link AxisDirection} for a given direction; we avoid potential differences like lower
* versus upper cases, amount of white space, etc.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(baseDirection.name());
toLowerCase(buffer, 0);
buffer.append(" along ");
final double md = Math.abs(meridian);
final int mi = (int) md;
if (md == mi) {
buffer.append(mi);
} else {
buffer.append(md);
}
buffer.append(" deg");
if (md != 0 && mi != 180) {
buffer.append(' ');
final int base = buffer.length();
final AxisDirection sign = meridian < 0 ? AxisDirection.WEST : AxisDirection.EAST;
buffer.append(sign.name());
toLowerCase(buffer, base);
}
final String name = buffer.toString();
assert EPSG.matcher(name).matches() : name;
return name;
}
/**
* Changes the buffer content to lower case from {@code base+1} to
* the end of the buffer. For {@link #toString} internal use only.
*/
private static void toLowerCase(final StringBuilder buffer, final int base) {
for (int i=buffer.length(); --i>base;) {
buffer.setCharAt(i, Character.toLowerCase(buffer.charAt(i)));
}
}
}