org.geotoolkit.referencing.cs.DirectionAlongMeridian Maven / Gradle / Ivy
Show all versions of geotk-referencing Show documentation
/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2007-2011, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2011, Geomatys
*
* 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.geotoolkit.referencing.cs;
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.jcip.annotations.Immutable;
import org.opengis.referencing.cs.AxisDirection;
import org.geotoolkit.internal.CodeLists;
import org.geotoolkit.internal.referencing.AxisDirections;
/**
* 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.
*
* @author Martin Desruisseaux (IRD)
* @version 3.02
*
* @since 2.4
* @module
*/
@Immutable
final class DirectionAlongMeridian implements Comparable, Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 1602711631943838328L;
/**
* For floating point comparisons.
*/
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*°)\\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 = Directions.find(BASE_DIRECTIONS, group);
if (baseDirection == null || !AxisDirection.NORTH.equals(AxisDirections.absolute(baseDirection))) {
// 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(3);
if (group != null) {
final AxisDirection sign = Directions.find(BASE_DIRECTIONS, group);
if (sign == null) {
return null;
}
final AxisDirection abs = AxisDirections.absolute(sign);
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);
}
/**
* 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() {
AxisDirection direction = this.direction;
if (direction == null) {
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 sufficient 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 Geotk implementation.
*/
direction = Directions.find(name);
if (direction == null) {
direction = CodeLists.valueOf(AxisDirection.class, name);
}
this.direction = direction;
}
}
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.
*
* 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 (AxisDirections.isOpposite(baseDirection)) {
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
*
*/
@Override
public int compareTo(final DirectionAlongMeridian that) {
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) + 31*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());
for (int i=buffer.length(); --i>0;) {
buffer.setCharAt(i, Character.toLowerCase(buffer.charAt(i)));
}
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('°');
if (md != 0 && md != 180) {
buffer.append(meridian < 0 ? 'W' : 'E');
}
final String name = buffer.toString();
assert EPSG.matcher(name).matches() : name;
return name;
}
}