001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2007, GeoTools Project Managment Committee (PMC)
005: * (C) 2007, Geomatys
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation;
010: * version 2.1 of the License.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.referencing.cs;
018:
019: // J2SE dependencies
020: import java.io.Serializable;
021: import java.util.regex.Matcher;
022: import java.util.regex.Pattern;
023:
024: // Geotools dependencies
025: import org.opengis.referencing.cs.AxisDirection;
026:
027: /**
028: * Parses {@linkplain AxisDirection axis direction} of the kind
029: * "<cite>South along 90 deg East</cite>". Those directions are
030: * used in the EPSG database for polar stereographic projections.
031: *
032: * @version $Id: DirectionAlongMeridian.java 28264 2007-12-05 21:53:08Z desruisseaux $
033: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/cs/DirectionAlongMeridian.java $
034: * @author Martin Desruisseaux
035: */
036: final class DirectionAlongMeridian implements Comparable, Serializable {
037: /**
038: * For cross-version compatibility.
039: */
040: private static final long serialVersionUID = 1602711631943838328L;
041:
042: /**
043: * For floating point comparaisons.
044: */
045: static final double EPS = 1E-10;
046:
047: /**
048: * A parser for EPSG axis names. Examples:
049: *
050: * "<cite>South along 180 deg</cite>",
051: * "<cite>South along 90 deg East</cite>"
052: */
053: private static final Pattern EPSG = Pattern
054: .compile(
055: "(\\p{Graph}+)\\s+along\\s+([\\-\\p{Digit}\\.]+)\\s+deg\\s*(\\p{Graph}+)?",
056: Pattern.CASE_INSENSITIVE);
057:
058: /**
059: * The base directions we are interrested in. Any direction not in
060: * this group will be rejected by our parser.
061: */
062: private static final AxisDirection[] BASE_DIRECTIONS = new AxisDirection[] {
063: AxisDirection.NORTH, AxisDirection.SOUTH,
064: AxisDirection.EAST, AxisDirection.WEST };
065:
066: /**
067: * The direction. Will be created only when first needed.
068: *
069: * @see #getDirection
070: */
071: private transient AxisDirection direction;
072:
073: /**
074: * The base direction, which must be {@link AxisDirection#NORTH} or
075: * {@link AxisDirection#SOUTH}.
076: */
077: public final AxisDirection baseDirection;
078:
079: /**
080: * The meridian, in degrees.
081: */
082: public final double meridian;
083:
084: /**
085: * Creates a direction.
086: */
087: private DirectionAlongMeridian(final AxisDirection baseDirection,
088: final double meridian) {
089: this .baseDirection = baseDirection;
090: this .meridian = meridian;
091: }
092:
093: /**
094: * Returns the dimension along meridian for the specified axis direction, or {@code null} if
095: * none.
096: */
097: public static DirectionAlongMeridian parse(
098: final AxisDirection direction) {
099: final DirectionAlongMeridian candidate = parse(direction.name());
100: if (candidate != null) {
101: candidate.direction = direction;
102: }
103: return candidate;
104: }
105:
106: /**
107: * If the specified name is a direction along some specific meridian,
108: * returns information about that. Otherwise returns {@code null}.
109: */
110: public static DirectionAlongMeridian parse(final String name) {
111: final Matcher m = EPSG.matcher(name);
112: if (!m.matches()) {
113: // Not the expected pattern.
114: return null;
115: }
116: String group = m.group(1);
117: final AxisDirection baseDirection = findDirection(
118: BASE_DIRECTIONS, group);
119: if (baseDirection == null
120: || !AxisDirection.NORTH
121: .equals(baseDirection.absolute())) {
122: // We expected "North" or "South" direction.
123: return null;
124: }
125: group = m.group(2);
126: double meridian;
127: try {
128: meridian = Double.parseDouble(group);
129: } catch (NumberFormatException exception) {
130: // Not a legal axis direction. Just ignore the exception,
131: // since we are supposed to return 'null' in this situation.
132: return null;
133: }
134: if (!(meridian >= -180 && meridian <= 180)) {
135: // Meridian is NaN or is not in the valid range.
136: return null;
137: }
138: group = m.group(3);
139: if (group != null) {
140: final AxisDirection sign = findDirection(BASE_DIRECTIONS,
141: group);
142: final AxisDirection abs = sign.absolute();
143: if (sign == null || !AxisDirection.EAST.equals(abs)) {
144: // We expected "East" or "West" direction.
145: return null;
146: }
147: if (sign != abs) {
148: meridian = -meridian;
149: }
150: }
151: return new DirectionAlongMeridian(baseDirection, meridian);
152: }
153:
154: /**
155: * Searchs for the specified name in the specified set of directions.
156: */
157: private static AxisDirection findDirection(
158: final AxisDirection[] values, final String direction) {
159: for (int i = 0; i < values.length; i++) {
160: final AxisDirection candidate = values[i];
161: final String name = candidate.name();
162: if (direction.equalsIgnoreCase(name)) {
163: return candidate;
164: }
165: }
166: return null;
167: }
168:
169: /**
170: * Searchs for the specified name.
171: */
172: static AxisDirection findDirection(String direction) {
173: final AxisDirection[] values = AxisDirection.values();
174: AxisDirection candidate = findDirection(values, direction);
175: if (candidate == null) {
176: String modified = direction.replace('-', '_');
177: if (modified != direction) {
178: direction = modified;
179: candidate = findDirection(values, modified);
180: }
181: if (candidate == null) {
182: modified = direction.replace(' ', '_');
183: if (modified != direction) {
184: candidate = findDirection(values, modified);
185: }
186: }
187: }
188: return candidate;
189: }
190:
191: /**
192: * Returns the axis direction for this object. If a suitable axis direction already exists,
193: * it will be returned. Otherwise a new one is created and returned.
194: */
195: public AxisDirection getDirection() {
196: if (direction != null) {
197: return direction;
198: }
199: final String name = toString();
200: synchronized (AxisDirection.class) {
201: /*
202: * The calls to 'AxisDirection.values()' and 'findDirection(...)' should be performed
203: * inside the synchronized block, since we try to avoid the creation of many directions
204: * for the same name. Strictly speaking, this synchronization is not suffisient since
205: * it doesn't apply to the creation of axis directions from outside this class. But it
206: * okay if this code is the only place where we create axis directions with name of the
207: * kind "South among 90�E". This assumption holds for Geotools implementation.
208: */
209: direction = findDirection(name);
210: if (direction == null) {
211: direction = AxisDirection.valueOf(name);
212: }
213: }
214: return direction;
215: }
216:
217: /**
218: * Returns the arithmetic (counterclockwise) angle from this direction to the specified
219: * direction, in decimal degrees. This method returns a value between -180� and +180�, or
220: * {@link Double#NaN NaN} if the {@linkplain #baseDirection base directions} don't match.
221: * A positive angle denote a right-handed system.
222: * <p>
223: * Example: the angle from "<cite>North along 90 deg East</cite>" to
224: * "<cite>North along 0 deg</cite> is 90�.
225: */
226: public double getAngle(final DirectionAlongMeridian other) {
227: if (!baseDirection.equals(other.baseDirection)) {
228: return Double.NaN;
229: }
230: /*
231: * We want the following pair of axis:
232: * (NORTH along 90�E, NORTH along 0�)
233: * to give a positive angle of 90�
234: */
235: double angle = meridian - other.meridian;
236: /*
237: * Forces to the [-180� .. +180�] range.
238: */
239: if (angle < -180) {
240: angle += 360;
241: } else if (angle > 180) {
242: angle -= 360;
243: }
244: /*
245: * Reverses the sign for axis oriented toward SOUTH,
246: * so a positive angle is a right-handed system.
247: */
248: if (!baseDirection.equals(baseDirection.absolute())) {
249: angle = -angle;
250: }
251: return angle;
252: }
253:
254: /**
255: * Compares this direction with the specified one for order. This method tries to reproduce
256: * the ordering used for the majority of coordinate systems in the EPSG database, i.e. the
257: * ordering of a right-handed coordinate system. Examples of ordered pairs that we should
258: * get (extracted from the EPSG database):
259: *
260: * <table>
261: * <tr><td>North along 90 deg East,</td> <td>North along 0 deg</td></tr>
262: * <tr><td>North along 75 deg West,</td> <td>North along 165 deg West</td></tr>
263: * <tr><td>South along 90 deg West,</td> <td>South along 0 deg</td></tr>
264: * <tr><td>South along 180 deg,</td> <td>South along 90 deg West</td></tr>
265: * <tr><td>North along 130 deg West</td> <td>North along 140 deg East</td></tr>
266: * </table>
267: */
268: public int compareTo(final Object object) {
269: final DirectionAlongMeridian that = (DirectionAlongMeridian) object;
270: final int c = baseDirection.compareTo(that.baseDirection);
271: if (c != 0) {
272: return c;
273: }
274: final double angle = getAngle(that);
275: if (angle < 0)
276: return +1; // Really the opposite sign.
277: if (angle > 0)
278: return -1; // Really the opposite sign.
279: return 0;
280: }
281:
282: /**
283: * Tests this object for equality with the specified one.
284: * This method is used mostly for assertions.
285: */
286: public boolean equals(final Object object) {
287: if (object instanceof DirectionAlongMeridian) {
288: final DirectionAlongMeridian that = (DirectionAlongMeridian) object;
289: return baseDirection.equals(that.baseDirection)
290: && Double.doubleToLongBits(meridian) == Double
291: .doubleToLongBits(that.meridian);
292: }
293: return false;
294: }
295:
296: /**
297: * Returns a hash code value, for consistency with {@link #equals}.
298: */
299: public int hashCode() {
300: final long code = Double.doubleToLongBits(meridian);
301: return (int) serialVersionUID ^ (int) code ^ (int) (code >> 32)
302: + 37 * baseDirection.hashCode();
303: }
304:
305: /**
306: * Returns a string representation of this direction, using a syntax matching the one used
307: * by EPSG. This string representation will be used for creating a new {@link AxisDirection}.
308: * The generated name should be identical to EPSG name, but we use the generated one anyway
309: * (rather than the one provided by EPSG) in order to make sure that we create a single
310: * {@link AxisDirection} for a given direction; we avoid potential differences like lower
311: * versus upper cases, amount of white space, <cite>etc</cite>.
312: */
313: public String toString() {
314: final StringBuffer buffer = new StringBuffer(baseDirection
315: .name());
316: toLowerCase(buffer, 0);
317: buffer.append(" along ");
318: final double md = Math.abs(meridian);
319: final int mi = (int) md;
320: if (md == mi) {
321: buffer.append(mi);
322: } else {
323: buffer.append(md);
324: }
325: buffer.append(" deg");
326: if (md != 0 && mi != 180) {
327: buffer.append(' ');
328: final int base = buffer.length();
329: final AxisDirection sign = meridian < 0 ? AxisDirection.WEST
330: : AxisDirection.EAST;
331: buffer.append(sign.name());
332: toLowerCase(buffer, base);
333: }
334: final String name = buffer.toString();
335: assert EPSG.matcher(name).matches() : name;
336: return name;
337: }
338:
339: /**
340: * Changes the buffer content to lower case from {@code base+1} to
341: * the end of the buffer. For {@link #toString} internal use only.
342: */
343: private static void toLowerCase(final StringBuffer buffer,
344: final int base) {
345: for (int i = buffer.length(); --i > base;) {
346: buffer
347: .setCharAt(i, Character.toLowerCase(buffer
348: .charAt(i)));
349: }
350: }
351: }
|