/*
* 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
* "<cite>South along 90 deg East</cite>". 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:
*
* "<cite>South along 180 deg</cite>",
* "<cite>South along 90 deg East</cite>"
*/
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<values.length; i++) {
final AxisDirection candidate = values[i];
final String name = candidate.name();
if (direction.equalsIgnoreCase(name)) {
return candidate;
}
// check for common abbreviations
if(direction.length() == 1) {
if(candidate == AxisDirection.NORTH && direction.equals("N"))
return candidate;
if(candidate == AxisDirection.SOUTH && direction.equals("S"))
return candidate;
if(candidate == AxisDirection.WEST && direction.equals("W"))
return candidate;
if(candidate == AxisDirection.EAST && direction.equals("E"))
return candidate;
}
}
return null;
}
/**
* Searchs for the specified name.
*/
static AxisDirection findDirection(String direction) {
final AxisDirection[] values = AxisDirection.values();
AxisDirection candidate = findDirection(values, direction);
if (candidate == null) {
String modified = direction.replace('-', '_');
if (modified != direction) {
direction = modified;
candidate = findDirection(values, modified);
}
if (candidate == null) {
modified = direction.replace(' ', '_');
if (modified != direction) {
candidate = findDirection(values, modified);
}
}
}
return candidate;
}
/**
* Returns the axis direction for this object. If a suitable axis direction already exists,
* it will be returned. Otherwise a new one is created and returned.
*/
public AxisDirection getDirection() {
if (direction != null) {
return direction;
}
final String name = toString();
synchronized (AxisDirection.class) {
/*
* The calls to 'AxisDirection.values()' and 'findDirection(...)' should be performed
* inside the synchronized block, since we try to avoid the creation of many directions
* for the same name. Strictly speaking, this synchronization is not suffisient since
* it doesn't apply to the creation of axis directions from outside this class. But it
* okay if this code is the only place where we create axis directions with name of the
* kind "South among 90°E". This assumption holds for Geotools implementation.
*/
direction = findDirection(name);
if (direction == null) {
direction = AxisDirection.valueOf(name);
}
}
return direction;
}
/**
* Returns the arithmetic (counterclockwise) angle from this direction to the specified
* direction, in decimal degrees. This method returns a value between -180° and +180°, or
* {@link Double#NaN NaN} if the {@linkplain #baseDirection base directions} don't match.
* A positive angle denote a right-handed system.
* <p>
* Example: the angle from "<cite>North along 90 deg East</cite>" to
* "<cite>North along 0 deg</cite> 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):
*
* <table>
* <tr><td>North along 90 deg East,</td> <td>North along 0 deg</td></tr>
* <tr><td>North along 75 deg West,</td> <td>North along 165 deg West</td></tr>
* <tr><td>South along 90 deg West,</td> <td>South along 0 deg</td></tr>
* <tr><td>South along 180 deg,</td> <td>South along 90 deg West</td></tr>
* <tr><td>North along 130 deg West</td> <td>North along 140 deg East</td></tr>
* </table>
*/
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, <cite>etc</cite>.
*/
@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)));
}
}
}