0001: /*
0002: * GeoTools - OpenSource mapping toolkit
0003: * http://geotools.org
0004: * (C) 2003-2006, GeoTools Project Managment Committee (PMC)
0005: * (C) 2001, Institut de Recherche pour le Développement
0006: * (C) 1999, Fisheries and Oceans Canada
0007: *
0008: * This library is free software; you can redistribute it and/or
0009: * modify it under the terms of the GNU Lesser General Public
0010: * License as published by the Free Software Foundation;
0011: * version 2.1 of the License.
0012: *
0013: * This library is distributed in the hope that it will be useful,
0014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0016: * Lesser General Public License for more details.
0017: */
0018: package org.geotools.measure;
0019:
0020: // J2SE dependencies
0021: import java.io.IOException;
0022: import java.io.ObjectInputStream;
0023: import java.text.DecimalFormat;
0024: import java.text.DecimalFormatSymbols;
0025: import java.text.FieldPosition;
0026: import java.text.Format;
0027: import java.text.ParseException;
0028: import java.text.ParsePosition;
0029: import java.util.Locale;
0030:
0031: // Geotools dependencies
0032: import org.geotools.resources.Utilities;
0033: import org.geotools.resources.XMath;
0034: import org.geotools.resources.i18n.Errors;
0035: import org.geotools.resources.i18n.ErrorKeys;
0036:
0037: /**
0038: * Parse and format angle according a specified pattern. The pattern is a string
0039: * containing any characters, with a special meaning for the following characters:
0040: *
0041: * <blockquote><table cellpadding="3">
0042: * <tr><td>{@code D}</td><td> The integer part of degrees</td></tr>
0043: * <tr><td>{@code d}</td><td> The fractional part of degrees</td></tr>
0044: * <tr><td>{@code M}</td><td> The integer part of minutes</td></tr>
0045: * <tr><td>{@code m}</td><td> The fractional part of minutes</td></tr>
0046: * <tr><td>{@code S}</td><td> The integer part of seconds</td></tr>
0047: * <tr><td>{@code s}</td><td> The fractional part of seconds</td></tr>
0048: * <tr><td>{@code .}</td><td> The decimal separator</td></tr>
0049: * </table></blockquote>
0050: * <br>
0051: * Upper-case letters {@code D}, {@code M} and {@code S} are for the integer
0052: * parts of degrees, minutes and seconds respectively. They must appear in this order (e.g.
0053: * "<code>M'D</code>" is illegal because "M" and "S" are inverted; "<code>D°S</code>" is
0054: * illegal too because there is no "M" between "D" and "S"). Lower-case letters {@code d},
0055: * {@code m} and {@code s} are for fractional parts of degrees, minutes and seconds
0056: * respectively. Only one of those may appears in a pattern, and it must be the last special
0057: * symbol (e.g. "<code>D.dd°MM'</code>" is illegal because "d" is followed by "M";
0058: * "{@code D.mm}" is illegal because "m" is not the fractional part of "D").
0059: * <br><br>
0060: * The number of occurrence of {@code D}, {@code M}, {@code S} and their
0061: * lower-case counterpart is the number of digits to format. For example, "DD.ddd" will
0062: * format angle with two digits for the integer part and three digits for the fractional
0063: * part (e.g. 4.4578 will be formatted as "04.458"). Separator characters like <code>°</code>,
0064: * <code>'</code> and <code>"</code> and inserted "as-is" in the formatted string (except the
0065: * decimal separator dot ("{@code .}"), which is replaced by the local-dependent decimal
0066: * separator). Separator characters may be completely omitted; {@code AngleFormat} will
0067: * still differentiate degrees, minutes and seconds fields according the pattern. For example,
0068: * "{@code 0480439}" with the pattern "{@code DDDMMmm}" will be parsed as 48°04.39'.
0069: * <br><br>
0070: * The following table gives some examples of legal patterns.
0071: *
0072: * <blockquote><table cellpadding="3">
0073: * <tr><th>Pattern </th> <th>Example </th></tr>
0074: * <tr><td><code>DD°MM'SS" </code></td> <td>48°30'00" </td></tr>
0075: * <tr><td><code>DD°MM' </code></td> <td>48°30' </td></tr>
0076: * <tr><td>{@code DD.ddd }</td> <td>48.500 </td></tr>
0077: * <tr><td>{@code DDMM }</td> <td>4830 </td></tr>
0078: * <tr><td>{@code DDMMSS }</td> <td>483000 </td></tr>
0079: * </table></blockquote>
0080: *
0081: * @see Angle
0082: * @see Latitude
0083: * @see Longitude
0084: *
0085: * @since 2.0
0086: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/measure/AngleFormat.java $
0087: * @version $Id: AngleFormat.java 20874 2006-08-07 10:00:01Z jgarnett $
0088: * @author Martin Desruisseaux
0089: */
0090: public class AngleFormat extends Format {
0091: /**
0092: * Caractère représentant l'hémisphère nord.
0093: * Il doit obligatoirement être en majuscule.
0094: */
0095: private static final char NORTH = 'N';
0096:
0097: /**
0098: * Caractère représentant l'hémisphère sud.
0099: * Il doit obligatoirement être en majuscule.
0100: */
0101: private static final char SOUTH = 'S';
0102:
0103: /**
0104: * Caractère représentant l'hémisphère est.
0105: * Il doit obligatoirement être en majuscule.
0106: */
0107: private static final char EAST = 'E';
0108:
0109: /**
0110: * Caractère représentant l'hémisphère ouest.
0111: * Il doit obligatoirement être en majuscule.
0112: */
0113: private static final char WEST = 'W';
0114:
0115: /**
0116: * Constante indique que l'angle
0117: * à formater est une longitude.
0118: */
0119: static final int LONGITUDE = 0;
0120:
0121: /**
0122: * Constante indique que l'angle
0123: * à formater est une latitude.
0124: */
0125: static final int LATITUDE = 1;
0126:
0127: /**
0128: * Constante indique que le nombre
0129: * à formater est une altitude.
0130: */
0131: static final int ALTITUDE = 2;
0132:
0133: /**
0134: * A constant for the symbol to appears before the degrees fields.
0135: * Fields PREFIX, DEGREES, MINUTES and SECONDS <strong>must</strong>
0136: * have increasing values (-1, 0, +1, +2, +3).
0137: */
0138: private static final int PREFIX_FIELD = -1;
0139:
0140: /**
0141: * Constant for degrees field. When formatting a string, this value may be
0142: * specified to the {@link java.text.FieldPosition} constructor in order to
0143: * get the bounding index where degrees have been written.
0144: */
0145: public static final int DEGREES_FIELD = 0;
0146:
0147: /**
0148: * Constant for minutes field. When formatting a string, this value may be
0149: * specified to the {@link java.text.FieldPosition} constructor in order to
0150: * get the bounding index where minutes have been written.
0151: */
0152: public static final int MINUTES_FIELD = 1;
0153:
0154: /**
0155: * Constant for seconds field. When formatting a string, this value may be
0156: * specified to the {@link java.text.FieldPosition} constructor in order to
0157: * get the bounding index where seconds have been written.
0158: */
0159: public static final int SECONDS_FIELD = 2;
0160:
0161: /**
0162: * Constant for hemisphere field. When formatting a string, this value may be
0163: * specified to the {@link java.text.FieldPosition} constructor in order to
0164: * get the bounding index where the hemisphere synbol has been written.
0165: */
0166: public static final int HEMISPHERE_FIELD = 3;
0167:
0168: /**
0169: * Symboles représentant les degrés (0),
0170: * minutes (1) et les secondes (2).
0171: */
0172: private static final char[] SYMBOLS = { 'D', 'M', 'S' };
0173:
0174: /**
0175: * Nombre minimal d'espaces que doivent occuper les parties
0176: * entières des degrés (0), minutes (1) et secondes (2). Le
0177: * champs {@code widthDecimal} indique la largeur fixe
0178: * que doit avoir la partie décimale. Il s'appliquera au
0179: * dernier champs non-zero dans {@code width0..2}.
0180: */
0181: private int width0 = 1, width1 = 2, width2 = 0, widthDecimal = 0;
0182:
0183: /**
0184: * Caractères à insérer au début ({@code prefix}) et à la
0185: * suite des degrés, minutes et secondes ({@code suffix0..2}).
0186: * Ces champs doivent être {@code null} s'il n'y a rien à insérer.
0187: */
0188: private String prefix = null, suffix0 = "\u00B0", suffix1 = "'",
0189: suffix2 = "\"";
0190:
0191: /**
0192: * Indique s'il faut utiliser le séparateur décimal pour séparer la partie
0193: * entière de la partie fractionnaire. La valeur {@code false} indique
0194: * que les parties entières et fractionnaires doivent être écrites ensembles
0195: * (par exemple 34867 pour 34.867). La valeur par défaut est {@code true}.
0196: */
0197: private boolean decimalSeparator = true;
0198:
0199: /**
0200: * Format à utiliser pour écrire les nombres
0201: * (degrés, minutes ou secondes) à l'intérieur
0202: * de l'écriture d'un angle.
0203: */
0204: private final DecimalFormat numberFormat;
0205:
0206: /**
0207: * Objet à transmetre aux méthodes {@code DecimalFormat.format}.
0208: * Ce paramètre existe simplement pour éviter de créer cet objet trop
0209: * souvent, alors qu'on ne s'y intéresse pas.
0210: */
0211: private transient FieldPosition dummy = new FieldPosition(0);
0212:
0213: /**
0214: * Restore fields after deserialization.
0215: */
0216: private void readObject(final ObjectInputStream in)
0217: throws IOException, ClassNotFoundException {
0218: in.defaultReadObject();
0219: dummy = new FieldPosition(0);
0220: }
0221:
0222: /**
0223: * Returns the width of the specified field.
0224: */
0225: private int getWidth(final int index) {
0226: switch (index) {
0227: case DEGREES_FIELD:
0228: return width0;
0229: case MINUTES_FIELD:
0230: return width1;
0231: case SECONDS_FIELD:
0232: return width2;
0233: default:
0234: return 0; // Must be 0 (important!)
0235: }
0236: }
0237:
0238: /**
0239: * Set the width for the specified field.
0240: * All folowing fields will be set to 0.
0241: */
0242: private void setWidth(final int index, int width) {
0243: switch (index) {
0244: case DEGREES_FIELD:
0245: width0 = width;
0246: width = 0; // fall through
0247: case MINUTES_FIELD:
0248: width1 = width;
0249: width = 0; // fall through
0250: case SECONDS_FIELD:
0251: width2 = width; // fall through
0252: }
0253: }
0254:
0255: /**
0256: * Returns the suffix for the specified field.
0257: */
0258: private String getSuffix(final int index) {
0259: switch (index) {
0260: case PREFIX_FIELD:
0261: return prefix;
0262: case DEGREES_FIELD:
0263: return suffix0;
0264: case MINUTES_FIELD:
0265: return suffix1;
0266: case SECONDS_FIELD:
0267: return suffix2;
0268: default:
0269: return null;
0270: }
0271: }
0272:
0273: /**
0274: * Set the suffix for the specified field. Suffix
0275: * for all following fields will be set to their
0276: * default value.
0277: */
0278: private void setSuffix(final int index, String s) {
0279: switch (index) {
0280: case PREFIX_FIELD:
0281: prefix = s;
0282: s = "\u00B0"; // fall through
0283: case DEGREES_FIELD:
0284: suffix0 = s;
0285: s = "'"; // fall through
0286: case MINUTES_FIELD:
0287: suffix1 = s;
0288: s = "\""; // fall through
0289: case SECONDS_FIELD:
0290: suffix2 = s; // fall through
0291: }
0292: }
0293:
0294: /**
0295: * Construct a new {@code AngleFormat} for the specified locale.
0296: */
0297: public static AngleFormat getInstance(final Locale locale) {
0298: return new AngleFormat("D\u00B0MM.m'", locale);
0299: }
0300:
0301: /**
0302: * Construct a new {@code AngleFormat} using
0303: * the current default locale and a default pattern.
0304: */
0305: public AngleFormat() {
0306: this ("D\u00B0MM.m'");
0307: }
0308:
0309: /**
0310: * Construct a new {@code AngleFormat} using the
0311: * current default locale and the specified pattern.
0312: *
0313: * @param pattern Pattern to use for parsing and formatting angle.
0314: * See class description for an explanation of how this pattern work.
0315: * @throws IllegalArgumentException If the specified pattern is not legal.
0316: */
0317: public AngleFormat(final String pattern)
0318: throws IllegalArgumentException {
0319: this (pattern, new DecimalFormatSymbols());
0320: }
0321:
0322: /**
0323: * Construct a new {@code AngleFormat}
0324: * using the specified pattern and locale.
0325: *
0326: * @param pattern Pattern to use for parsing and formatting angle.
0327: * See class description for an explanation of how this pattern work.
0328: * @param locale Locale to use.
0329: * @throws IllegalArgumentException If the specified pattern is not legal.
0330: */
0331: public AngleFormat(final String pattern, final Locale locale)
0332: throws IllegalArgumentException {
0333: this (pattern, new DecimalFormatSymbols(locale));
0334: }
0335:
0336: /**
0337: * Construct a new {@code AngleFormat}
0338: * using the specified pattern and decimal symbols.
0339: *
0340: * @param pattern Pattern to use for parsing and formatting angle.
0341: * See class description for an explanation of how this pattern work.
0342: * @param symbols The symbols to use for parsing and formatting numbers.
0343: * @throws IllegalArgumentException If the specified pattern is not legal.
0344: */
0345: public AngleFormat(final String pattern,
0346: final DecimalFormatSymbols symbols) {
0347: // NOTE: pour cette routine, il ne faut PAS que DecimalFormat
0348: // reconnaisse la notation exponentielle, parce que ça
0349: // risquerait d'être confondu avec le "E" de "Est".
0350: numberFormat = new DecimalFormat("#0", symbols);
0351: applyPattern(pattern);
0352: }
0353:
0354: /**
0355: * Set the pattern to use for parsing and formatting angle.
0356: * See class description for an explanation of how patterns work.
0357: *
0358: * @param pattern Pattern to use for parsing and formatting angle.
0359: * @throws IllegalArgumentException If the specified pattern is not legal.
0360: */
0361: public synchronized void applyPattern(final String pattern)
0362: throws IllegalArgumentException {
0363: widthDecimal = 0;
0364: decimalSeparator = true;
0365: int startPrefix = 0;
0366: int symbolIndex = 0;
0367: boolean parseFinished = false;
0368: final int length = pattern.length();
0369: for (int i = 0; i < length; i++) {
0370: /*
0371: * On examine un à un tous les caractères du patron en
0372: * sautant ceux qui ne sont pas réservés ("D", "M", "S"
0373: * et leur équivalents en minuscules). Les caractères
0374: * non-reservés seront mémorisés comme suffix plus tard.
0375: */
0376: final char c = pattern.charAt(i);
0377: final char upperCaseC = Character.toUpperCase(c);
0378: for (int field = DEGREES_FIELD; field < SYMBOLS.length; field++) {
0379: if (upperCaseC == SYMBOLS[field]) {
0380: /*
0381: * Un caractère réservé a été trouvé. Vérifie maintenant
0382: * s'il est valide. Par exemple il serait illegal d'avoir
0383: * comme patron "MM.mm" sans qu'il soit précédé des degrés.
0384: * On attend les lettres "D", "M" et "S" dans l'ordre. Si
0385: * le caractère est en lettres minuscules, il doit être le
0386: * même que le dernier code (par exemple "DD.mm" est illegal).
0387: */
0388: if (c == upperCaseC) {
0389: symbolIndex++;
0390: }
0391: if (field != symbolIndex - 1 || parseFinished) {
0392: setWidth(DEGREES_FIELD, 1);
0393: setSuffix(PREFIX_FIELD, null);
0394: widthDecimal = 0;
0395: decimalSeparator = true;
0396: throw new IllegalArgumentException(
0397: Errors
0398: .format(
0399: ErrorKeys.ILLEGAL_ANGLE_PATTERN_$1,
0400: pattern));
0401: }
0402: if (c == upperCaseC) {
0403: /*
0404: * Mémorise les caractères qui précédaient ce code comme suffix
0405: * du champs précédent. Puis on comptera le nombre de fois que le
0406: * code se répète, en mémorisant cette information comme largeur
0407: * de ce champ.
0408: */
0409: setSuffix(field - 1,
0410: (i > startPrefix) ? pattern.substring(
0411: startPrefix, i) : null);
0412: int w = 1;
0413: while (++i < length && pattern.charAt(i) == c)
0414: w++;
0415: setWidth(field, w);
0416: } else {
0417: /*
0418: * Si le caractère est une minuscule, ce qui le précédait sera le
0419: * séparateur décimal plutôt qu'un suffix. On comptera le nombre
0420: * d'occurences du caractères pour obtenir la précision.
0421: */
0422: switch (i - startPrefix) {
0423: case 0:
0424: decimalSeparator = false;
0425: break;
0426: case 1:
0427: if (pattern.charAt(startPrefix) == '.') {
0428: decimalSeparator = true;
0429: break;
0430: }
0431: default:
0432: throw new IllegalArgumentException(
0433: Errors
0434: .format(
0435: ErrorKeys.ILLEGAL_ANGLE_PATTERN_$1,
0436: pattern));
0437: }
0438: int w = 1;
0439: while (++i < length && pattern.charAt(i) == c)
0440: w++;
0441: widthDecimal = w;
0442: parseFinished = true;
0443: }
0444: startPrefix = i--;
0445: break; // Break 'j' and continue 'i'.
0446: }
0447: }
0448: }
0449: setSuffix(symbolIndex - 1, (startPrefix < length) ? pattern
0450: .substring(startPrefix) : null);
0451: }
0452:
0453: /**
0454: * Returns the pattern used for parsing and formatting angles.
0455: * See class description for an explanation of how patterns work.
0456: */
0457: public synchronized String toPattern() {
0458: char symbol = '#';
0459: final StringBuffer buffer = new StringBuffer();
0460: for (int field = DEGREES_FIELD; field <= SYMBOLS.length; field++) {
0461: final String previousSuffix = getSuffix(field - 1);
0462: int w = getWidth(field);
0463: if (w > 0) {
0464: /*
0465: * Procède à l'écriture de la partie entière des degrés,
0466: * minutes ou secondes. Le suffix du champs précédent
0467: * sera écrit avant les degrés, minutes ou secondes.
0468: */
0469: if (previousSuffix != null) {
0470: buffer.append(previousSuffix);
0471: }
0472: symbol = SYMBOLS[field];
0473: do
0474: buffer.append(symbol);
0475: while (--w > 0);
0476: } else {
0477: /*
0478: * Procède à l'écriture de la partie décimale des
0479: * degrés, minutes ou secondes. Le suffix du ce
0480: * champs sera écrit après cette partie fractionnaire.
0481: */
0482: w = widthDecimal;
0483: if (w > 0) {
0484: if (decimalSeparator)
0485: buffer.append('.');
0486: symbol = Character.toLowerCase(symbol);
0487: do
0488: buffer.append(symbol);
0489: while (--w > 0);
0490: }
0491: if (previousSuffix != null) {
0492: buffer.append(previousSuffix);
0493: }
0494: break;
0495: }
0496: }
0497: return buffer.toString();
0498: }
0499:
0500: /**
0501: * Format an angle. The string will be formatted according
0502: * the pattern set in the last call to {@link #applyPattern}.
0503: *
0504: * @param angle Angle to format, in degrees.
0505: * @return The formatted string.
0506: */
0507: public final String format(final double angle) {
0508: return format(angle, new StringBuffer(), null).toString();
0509: }
0510:
0511: /**
0512: * Formats an angle and appends the resulting text to a given string buffer.
0513: * The string will be formatted according the pattern set in the last call
0514: * to {@link #applyPattern}.
0515: *
0516: * @param angle Angle to format, in degrees.
0517: * @param toAppendTo Where the text is to be appended.
0518: * @param pos An optional {@link FieldPosition} identifying a field
0519: * in the formatted text, or {@code null} if this
0520: * information is not wanted. This field position shall
0521: * be constructed with one of the following constants:
0522: * {@link #DEGREES_FIELD},
0523: * {@link #MINUTES_FIELD},
0524: * {@link #SECONDS_FIELD} or
0525: * {@link #HEMISPHERE_FIELD}.
0526: *
0527: * @return The string buffer passed in as {@code toAppendTo}, with formatted text appended.
0528: */
0529: public synchronized StringBuffer format(final double angle,
0530: StringBuffer toAppendTo, final FieldPosition pos) {
0531: double degrees = angle;
0532: /*
0533: * Calcule à l'avance les minutes et les secondes. Si les minutes et secondes
0534: * ne doivent pas être écrits, on mémorisera NaN. Notez que pour extraire les
0535: * parties entières, on utilise (int) au lieu de 'Math.floor' car (int) arrondie
0536: * vers 0 (ce qui est le comportement souhaité) alors que 'floor' arrondie vers
0537: * l'entier inférieur.
0538: */
0539: double minutes = Double.NaN;
0540: double secondes = Double.NaN;
0541: if (width1 != 0 && !Double.isNaN(angle)) {
0542: int tmp = (int) degrees; // Arrondie vers 0 même si négatif.
0543: minutes = Math.abs(degrees - tmp) * 60;
0544: degrees = tmp;
0545: if (minutes < 0 || minutes > 60) {
0546: // Erreur d'arrondissement (parce que l'angle est trop élevé)
0547: throw new IllegalArgumentException(Errors.format(
0548: ErrorKeys.ANGLE_OVERFLOW_$1, new Double(angle)));
0549: }
0550: if (width2 != 0) {
0551: tmp = (int) minutes; // Arrondie vers 0 même si négatif.
0552: secondes = (minutes - tmp) * 60;
0553: minutes = tmp;
0554: if (secondes < 0 || secondes > 60) {
0555: // Erreur d'arrondissement (parce que l'angle est trop élevé)
0556: throw new IllegalArgumentException(Errors.format(
0557: ErrorKeys.ANGLE_OVERFLOW_$1, new Double(
0558: angle)));
0559: }
0560: /*
0561: * On applique maintenant une correction qui tiendra
0562: * compte des problèmes d'arrondissements.
0563: */
0564: final double puissance = XMath.pow10(widthDecimal);
0565: secondes = Math.rint(secondes * puissance) / puissance;
0566: tmp = (int) (secondes / 60);
0567: secondes -= 60 * tmp;
0568: minutes += tmp;
0569: } else {
0570: final double puissance = XMath.pow10(widthDecimal);
0571: minutes = Math.rint(minutes * puissance) / puissance;
0572: }
0573: tmp = (int) (minutes / 60); // Arrondie vers 0 même si négatif.
0574: minutes -= 60 * tmp;
0575: degrees += tmp;
0576: }
0577: /*
0578: * Les variables 'degrés', 'minutes' et 'secondes' contiennent
0579: * maintenant les valeurs des champs à écrire, en principe épurés
0580: * des problèmes d'arrondissements. Procède maintenant à l'écriture
0581: * de l'angle.
0582: */
0583: if (prefix != null) {
0584: toAppendTo.append(prefix);
0585: }
0586: final int field;
0587: if (pos != null) {
0588: field = pos.getField();
0589: pos.setBeginIndex(0);
0590: pos.setEndIndex(0);
0591: } else {
0592: field = PREFIX_FIELD;
0593: }
0594: toAppendTo = formatField(degrees, toAppendTo,
0595: field == DEGREES_FIELD ? pos : null, width0,
0596: width1 == 0, suffix0);
0597: if (!Double.isNaN(minutes)) {
0598: toAppendTo = formatField(minutes, toAppendTo,
0599: field == MINUTES_FIELD ? pos : null, width1,
0600: width2 == 0, suffix1);
0601: }
0602: if (!Double.isNaN(secondes)) {
0603: toAppendTo = formatField(secondes, toAppendTo,
0604: field == SECONDS_FIELD ? pos : null, width2, true,
0605: suffix2);
0606: }
0607: return toAppendTo;
0608: }
0609:
0610: /**
0611: * Procède à l'écriture d'un champ de l'angle.
0612: *
0613: * @param value Valeur à écrire.
0614: * @param toAppendTo Buffer dans lequel écrire le champs.
0615: * @param pos Objet dans lequel mémoriser les index des premiers
0616: * et derniers caractères écrits, ou {@code null}
0617: * pour ne pas mémoriser ces index.
0618: * @param w Nombre de minimal caractères de la partie entière.
0619: * @param last {@code true} si ce champs est le dernier,
0620: * et qu'il faut donc écrire la partie décimale.
0621: * @param s Suffix à écrire après le nombre (peut être nul).
0622: */
0623: private StringBuffer formatField(double value,
0624: StringBuffer toAppendTo, final FieldPosition pos,
0625: final int w, final boolean last, final String s) {
0626: final int startPosition = toAppendTo.length();
0627: if (!last) {
0628: numberFormat.setMinimumIntegerDigits(w);
0629: numberFormat.setMaximumFractionDigits(0);
0630: toAppendTo = numberFormat.format(value, toAppendTo, dummy);
0631: } else if (decimalSeparator) {
0632: numberFormat.setMinimumIntegerDigits(w);
0633: numberFormat.setMinimumFractionDigits(widthDecimal);
0634: numberFormat.setMaximumFractionDigits(widthDecimal);
0635: toAppendTo = numberFormat.format(value, toAppendTo, dummy);
0636: } else {
0637: value *= XMath.pow10(widthDecimal);
0638: numberFormat.setMaximumFractionDigits(0);
0639: numberFormat.setMinimumIntegerDigits(w + widthDecimal);
0640: toAppendTo = numberFormat.format(value, toAppendTo, dummy);
0641: }
0642: if (s != null) {
0643: toAppendTo.append(s);
0644: }
0645: if (pos != null) {
0646: pos.setBeginIndex(startPosition);
0647: pos.setEndIndex(toAppendTo.length() - 1);
0648: }
0649: return toAppendTo;
0650: }
0651:
0652: /**
0653: * Formats an angle, a latitude or a longitude and appends the resulting text
0654: * to a given string buffer. The string will be formatted according the pattern
0655: * set in the last call to {@link #applyPattern}. The argument {@code obj}
0656: * shall be an {@link Angle} object or one of its derived class ({@link Latitude},
0657: * {@link Longitude}). If {@code obj} is a {@link Latitude} object, then a
0658: * symbol "N" or "S" will be appended to the end of the string (the symbol will
0659: * be choosen according the angle's sign). Otherwise, if {@code obj} is a
0660: * {@link Longitude} object, then a symbol "E" or "W" will be appended to the
0661: * end of the string. Otherwise, no hemisphere symbol will be appended.
0662: * <br><br>
0663: * Strictly speaking, formatting ordinary numbers is not the
0664: * {@code AngleFormat}'s job. Nevertheless, this method
0665: * accept {@link Number} objects. This capability is provided
0666: * only as a convenient way to format altitude numbers together
0667: * with longitude and latitude angles.
0668: *
0669: * @param obj {@link Angle} or {@link Number} object to format.
0670: * @param toAppendTo Where the text is to be appended.
0671: * @param pos An optional {@link FieldPosition} identifying a field
0672: * in the formatted text, or {@code null} if this
0673: * information is not wanted. This field position shall
0674: * be constructed with one of the following constants:
0675: * {@link #DEGREES_FIELD},
0676: * {@link #MINUTES_FIELD},
0677: * {@link #SECONDS_FIELD} or
0678: * {@link #HEMISPHERE_FIELD}.
0679: *
0680: * @return The string buffer passed in as {@code toAppendTo}, with
0681: * formatted text appended.
0682: * @throws IllegalArgumentException if {@code obj} if not an object
0683: * of class {@link Angle} or {@link Number}.
0684: */
0685: public synchronized StringBuffer format(final Object obj,
0686: StringBuffer toAppendTo, final FieldPosition pos)
0687: throws IllegalArgumentException {
0688: if (obj instanceof Latitude) {
0689: return format(((Latitude) obj).degrees(), toAppendTo, pos,
0690: NORTH, SOUTH);
0691: }
0692: if (obj instanceof Longitude) {
0693: return format(((Longitude) obj).degrees(), toAppendTo, pos,
0694: EAST, WEST);
0695: }
0696: if (obj instanceof Angle) {
0697: return format(((Angle) obj).degrees(), toAppendTo, pos);
0698: }
0699: if (obj instanceof Number) {
0700: numberFormat.setMinimumIntegerDigits(1);
0701: numberFormat.setMinimumFractionDigits(0);
0702: numberFormat.setMaximumFractionDigits(2);
0703: return numberFormat.format(obj, toAppendTo,
0704: (pos != null) ? pos : dummy);
0705: }
0706: throw new IllegalArgumentException(Errors.format(
0707: ErrorKeys.NOT_AN_ANGLE_OBJECT_$1, Utilities
0708: .getShortClassName(obj)));
0709: }
0710:
0711: /**
0712: * Procède à l'écriture d'un angle, d'une latitude ou d'une longitude.
0713: *
0714: * @param number Angle ou nombre à écrire.
0715: * @param type Type de l'angle ou du nombre:
0716: * {@link #LONGITUDE},
0717: * {@link #LATITUDE} ou
0718: * {@link #ALTITUDE}.
0719: * @param toAppendTo Buffer dans lequel écrire l'angle.
0720: * @param pos En entré, le code du champs dont on désire les index
0721: * ({@link #DEGREES_FIELD},
0722: * {@link #MINUTES_FIELD},
0723: * {@link #SECONDS_FIELD} ou
0724: * {@link #HEMISPHERE_FIELD}).
0725: * En sortie, les index du champs demandé. Ce paramètre
0726: * peut être nul si cette information n'est pas désirée.
0727: *
0728: * @return Le buffer {@code toAppendTo} par commodité.
0729: */
0730: synchronized StringBuffer format(final double number,
0731: final int type, StringBuffer toAppendTo,
0732: final FieldPosition pos) {
0733: switch (type) {
0734: default:
0735: throw new IllegalArgumentException(Integer.toString(type)); // Should not happen.
0736: case LATITUDE:
0737: return format(number, toAppendTo, pos, NORTH, SOUTH);
0738: case LONGITUDE:
0739: return format(number, toAppendTo, pos, EAST, WEST);
0740: case ALTITUDE: {
0741: numberFormat.setMinimumIntegerDigits(1);
0742: numberFormat.setMinimumFractionDigits(0);
0743: numberFormat.setMaximumFractionDigits(2);
0744: return numberFormat.format(number, toAppendTo,
0745: (pos != null) ? pos : dummy);
0746: }
0747: }
0748: }
0749:
0750: /**
0751: * Procède à l'écriture d'un angle suivit d'un suffix 'N','S','E' ou 'W'.
0752: * L'angle sera formaté en utilisant comme modèle le patron spécifié lors
0753: * du dernier appel de la méthode {@link #applyPattern}.
0754: *
0755: * @param angle Angle à écrire, en degrés.
0756: * @param toAppendTo Buffer dans lequel écrire l'angle.
0757: * @param pos En entré, le code du champs dont on désire les index
0758: * ({@link #DEGREES_FIELD},
0759: * {@link #MINUTES_FIELD},
0760: * {@link #SECONDS_FIELD} ou
0761: * {@link #HEMISPHERE_FIELD}).
0762: * En sortie, les index du champs demandé. Ce paramètre
0763: * peut être nul si cette information n'est pas désirée.
0764: * @param north Caractères à écrire si l'angle est positif ou nul.
0765: * @param south Caractères à écrire si l'angle est négatif.
0766: *
0767: * @return Le buffer {@code toAppendTo} par commodité.
0768: */
0769: private StringBuffer format(final double angle,
0770: StringBuffer toAppendTo, final FieldPosition pos,
0771: final char north, final char south) {
0772: toAppendTo = format(Math.abs(angle), toAppendTo, pos);
0773: final int start = toAppendTo.length();
0774: toAppendTo.append(angle < 0 ? south : north);
0775: if (pos != null && pos.getField() == HEMISPHERE_FIELD) {
0776: pos.setBeginIndex(start);
0777: pos.setEndIndex(toAppendTo.length() - 1);
0778: }
0779: return toAppendTo;
0780: }
0781:
0782: /**
0783: * Ignore le suffix d'un nombre. Cette méthode est appellée par la méthode
0784: * {@link #parse} pour savoir quel champs il vient de lire. Par exemple si
0785: * l'on vient de lire les degrés dans "48°12'", alors cette méthode extraira
0786: * le "°" et retournera 0 pour indiquer que l'on vient de lire des degrés.
0787: *
0788: * Cette méthode se chargera d'ignorer les espaces qui précèdent le suffix.
0789: * Elle tentera ensuite de d'abord interpréter le suffix selon les symboles
0790: * du patron (spécifié avec {@link #applyPattern}. Si le suffix n'a pas été
0791: * reconnus, elle tentera ensuite de le comparer aux symboles standards
0792: * (° ' ").
0793: *
0794: * @param source Chaîne dans laquelle on doit sauter le suffix.
0795: * @param pos En entré, l'index du premier caractère à considérer dans la
0796: * chaîne {@code pos}. En sortie, l'index du premier caractère
0797: * suivant le suffix (c'est-à-dire index à partir d'où continuer la
0798: * lecture après l'appel de cette méthode). Si le suffix n'a pas été
0799: * reconnu, alors cette méthode retourne par convention {@code SYMBOLS.length}.
0800: * @param field Champs à vérifier de préférences. Par exemple la valeur 1 signifie que les
0801: * suffix des minutes et des secondes devront être vérifiés avant celui des degrés.
0802: * @return Le numéro du champs correspondant au suffix qui vient d'être extrait:
0803: * -1 pour le préfix de l'angle, 0 pour le suffix des degrés, 1 pour le
0804: * suffix des minutes et 2 pour le suffix des secondes. Si le texte n'a
0805: * pas été reconnu, retourne {@code SYMBOLS.length}.
0806: */
0807: private int skipSuffix(final String source,
0808: final ParsePosition pos, int field) {
0809: /*
0810: * Essaie d'abord de sauter les suffix qui
0811: * avaient été spécifiés dans le patron.
0812: */
0813: final int length = source.length();
0814: int start = pos.getIndex();
0815: for (int j = SYMBOLS.length; j >= 0; j--) { // C'est bien j>=0 et non j>0.
0816: int index = start;
0817: final String toSkip = getSuffix(field);
0818: if (toSkip != null) {
0819: final int toSkipLength = toSkip.length();
0820: do {
0821: if (source.regionMatches(index, toSkip, 0,
0822: toSkipLength)) {
0823: pos.setIndex(index + toSkipLength);
0824: return field;
0825: }
0826: } while (index < length
0827: && Character
0828: .isSpaceChar(source.charAt(index++)));
0829: }
0830: if (++field >= SYMBOLS.length)
0831: field = -1;
0832: }
0833: /*
0834: * Le texte trouvé ne correspondant à aucun suffix du patron,
0835: * essaie maintenant de sauter un des suffix standards (après
0836: * avoir ignoré les espaces qui le précédaient).
0837: */
0838: char c;
0839: do {
0840: if (start >= length) {
0841: return SYMBOLS.length;
0842: }
0843: } while (Character.isSpaceChar(c = source.charAt(start++)));
0844: switch (c) {
0845: case '\u00B0':
0846: pos.setIndex(start);
0847: return DEGREES_FIELD;
0848: case '\'':
0849: pos.setIndex(start);
0850: return MINUTES_FIELD;
0851: case '"':
0852: pos.setIndex(start);
0853: return SECONDS_FIELD;
0854: default:
0855: return SYMBOLS.length; // Unknow field.
0856: }
0857: }
0858:
0859: /**
0860: * Parse a string as an angle. This method can parse an angle even if it
0861: * doesn't comply exactly to the expected pattern. For example, this method
0862: * will parse correctly string "<code>48°12.34'</code>" even if the expected
0863: * pattern was "{@code DDMM.mm}" (i.e. the string should have been
0864: * "{@code 4812.34}"). Spaces between degrees, minutes and secondes
0865: * are ignored. If the string ends with an hemisphere symbol "N" or "S",
0866: * then this method returns an object of class {@link Latitude}. Otherwise,
0867: * if the string ends with an hemisphere symbol "E" or "W", then this method
0868: * returns an object of class {@link Longitude}. Otherwise, this method
0869: * returns an object of class {@link Angle}.
0870: *
0871: * @param source A String whose beginning should be parsed.
0872: * @param pos Position where to start parsing.
0873: * @return The parsed string as an {@link Angle}, {@link Latitude}
0874: * or {@link Longitude} object.
0875: */
0876: public Angle parse(final String source, final ParsePosition pos) {
0877: return parse(source, pos, false);
0878: }
0879:
0880: /**
0881: * Interprète une chaîne de caractères représentant un angle. Les règles
0882: * d'interprétation de cette méthode sont assez souples. Par exemple cettte
0883: * méthode interprétera correctement la chaîne "48°12.34'" même si le patron
0884: * attendu était "DDMM.mm" (c'est-à-dire que la chaîne aurait du être "4812.34").
0885: * Les espaces entre les degrés, minutes et secondes sont acceptés. Si l'angle
0886: * est suivit d'un symbole "N" ou "S", alors l'objet retourné sera de la classe
0887: * {@link Latitude}. S'il est plutot suivit d'un symbole "E" ou "W", alors l'objet
0888: * retourné sera de la classe {@link Longitude}. Sinon, il sera de la classe
0889: * {@link Angle}.
0890: *
0891: * @param source Chaîne de caractères à lire.
0892: * @param pos Position à partir d'où interpréter la chaîne.
0893: * @param spaceAsSeparator Indique si l'espace est accepté comme séparateur
0894: * à l'intérieur d'un angle. La valeur {@code true}
0895: * fait que l'angle "45 30" sera interprété comme "45°30".
0896: * @return L'angle lu.
0897: */
0898: private synchronized Angle parse(final String source,
0899: final ParsePosition pos, final boolean spaceAsSeparator) {
0900: double degrees = Double.NaN;
0901: double minutes = Double.NaN;
0902: double secondes = Double.NaN;
0903: final int length = source.length();
0904: ///////////////////////////////////////////////////////////////////////////////
0905: // BLOC A: Analyse la chaîne de caractères 'source' et affecte aux variables //
0906: // 'degrés', 'minutes' et 'secondes' les valeurs appropriées. //
0907: // Les premières accolades ne servent qu'à garder locales //
0908: // les variables sans intérêt une fois la lecture terminée. //
0909: ///////////////////////////////////////////////////////////////////////////////
0910: {
0911: /*
0912: * Extrait le préfix, s'il y en avait un. Si on tombe sur un symbole des
0913: * degrés, minutes ou secondes alors qu'on n'a pas encore lu de nombre,
0914: * on considèrera que la lecture a échouée.
0915: */
0916: final int indexStart = pos.getIndex();
0917: int index = skipSuffix(source, pos, PREFIX_FIELD);
0918: if (index >= 0 && index < SYMBOLS.length) {
0919: pos.setErrorIndex(indexStart);
0920: pos.setIndex(indexStart);
0921: return null;
0922: }
0923: /*
0924: * Saute les espaces blancs qui
0925: * précèdent le champs des degrés.
0926: */
0927: index = pos.getIndex();
0928: while (index < length
0929: && Character.isSpaceChar(source.charAt(index)))
0930: index++;
0931: pos.setIndex(index);
0932: /*
0933: * Lit les degrés. Notez que si aucun séparateur ne séparait les degrés
0934: * des minutes des secondes, alors cette lecture pourra inclure plusieurs
0935: * champs (exemple: "DDDMMmmm"). La séparation sera faite plus tard.
0936: */
0937: Number fieldObject = numberFormat.parse(source, pos);
0938: if (fieldObject == null) {
0939: pos.setIndex(indexStart);
0940: if (pos.getErrorIndex() < indexStart) {
0941: pos.setErrorIndex(index);
0942: }
0943: return null;
0944: }
0945: degrees = fieldObject.doubleValue();
0946: int indexEndField = pos.getIndex();
0947: boolean swapDM = true;
0948: BigBoss: switch (skipSuffix(source, pos, DEGREES_FIELD)) {
0949: /* ----------------------------------------------
0950: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉS DEGRÉS
0951: * ----------------------------------------------
0952: * Les degrés étaient suivit du préfix d'un autre angle. Le préfix sera donc
0953: * retourné dans le buffer pour un éventuel traitement par le prochain appel
0954: * à la méthode 'parse' et on n'ira pas plus loin dans l'analyse de la chaîne.
0955: */
0956: case PREFIX_FIELD: {
0957: pos.setIndex(indexEndField);
0958: break BigBoss;
0959: }
0960: /* ----------------------------------------------
0961: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉS DEGRÉS
0962: * ----------------------------------------------
0963: * On a trouvé le symbole des secondes au lieu de celui des degrés. On fait
0964: * la correction dans les variables 'degrés' et 'secondes' et on considère
0965: * que la lecture est terminée.
0966: */
0967: case SECONDS_FIELD: {
0968: secondes = degrees;
0969: degrees = Double.NaN;
0970: break BigBoss;
0971: }
0972: /* ----------------------------------------------
0973: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉS DEGRÉS
0974: * ----------------------------------------------
0975: * Aucun symbole ne suit les degrés. Des minutes sont-elles attendues?
0976: * Si oui, on fera comme si le symbole des degrés avait été là. Sinon,
0977: * on considèrera que la lecture est terminée.
0978: */
0979: default: {
0980: if (width1 == 0)
0981: break BigBoss;
0982: if (!spaceAsSeparator)
0983: break BigBoss;
0984: // fall through
0985: }
0986: /* ----------------------------------------------
0987: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉS DEGRÉS
0988: * ----------------------------------------------
0989: * Un symbole des degrés a été explicitement trouvé. Les degrés sont peut-être
0990: * suivit des minutes. On procèdera donc à la lecture du prochain nombre, puis
0991: * à l'analyse du symbole qui le suit.
0992: */
0993: case DEGREES_FIELD: {
0994: final int indexStartField = index = pos.getIndex();
0995: while (index < length
0996: && Character.isSpaceChar(source.charAt(index))) {
0997: index++;
0998: }
0999: if (!spaceAsSeparator && index != indexStartField) {
1000: break BigBoss;
1001: }
1002: pos.setIndex(index);
1003: fieldObject = numberFormat.parse(source, pos);
1004: if (fieldObject == null) {
1005: pos.setIndex(indexStartField);
1006: break BigBoss;
1007: }
1008: indexEndField = pos.getIndex();
1009: minutes = fieldObject.doubleValue();
1010: switch (skipSuffix(source, pos,
1011: (width1 != 0) ? MINUTES_FIELD : PREFIX_FIELD)) {
1012: /* ------------------------------------------------
1013: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES MINUTES
1014: * ------------------------------------------------
1015: * Le symbole trouvé est bel et bien celui des minutes.
1016: * On continuera le bloc pour tenter de lire les secondes.
1017: */
1018: case MINUTES_FIELD: {
1019: break; // continue outer switch
1020: }
1021: /* ------------------------------------------------
1022: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES MINUTES
1023: * ------------------------------------------------
1024: * Un symbole des secondes a été trouvé au lieu du symbole des minutes
1025: * attendu. On fera la modification dans les variables 'secondes' et
1026: * 'minutes' et on considèrera la lecture terminée.
1027: */
1028: case SECONDS_FIELD: {
1029: secondes = minutes;
1030: minutes = Double.NaN;
1031: break BigBoss;
1032: }
1033: /* ------------------------------------------------
1034: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES MINUTES
1035: * ------------------------------------------------
1036: * Aucun symbole n'a été trouvé. Les minutes étaient-elles attendues?
1037: * Si oui, on les acceptera et on tentera de lire les secondes. Si non,
1038: * on retourne le texte lu dans le buffer et on termine la lecture.
1039: */
1040: default: {
1041: if (width1 != 0)
1042: break; // Continue outer switch
1043: // fall through
1044: }
1045: /* ------------------------------------------------
1046: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES MINUTES
1047: * ------------------------------------------------
1048: * Au lieu des minutes, le symbole lu est celui des degrés. On considère
1049: * qu'il appartient au prochain angle. On retournera donc le texte lu dans
1050: * le buffer et on terminera la lecture.
1051: */
1052: case DEGREES_FIELD: {
1053: pos.setIndex(indexStartField);
1054: minutes = Double.NaN;
1055: break BigBoss;
1056: }
1057: /* ------------------------------------------------
1058: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES MINUTES
1059: * ------------------------------------------------
1060: * Après les minutes (qu'on accepte), on a trouvé le préfix du prochain
1061: * angle à lire. On retourne ce préfix dans le buffer et on considère la
1062: * lecture terminée.
1063: */
1064: case PREFIX_FIELD: {
1065: pos.setIndex(indexEndField);
1066: break BigBoss;
1067: }
1068: }
1069: swapDM = false;
1070: // fall through
1071: }
1072: /* ----------------------------------------------
1073: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉS DEGRÉS
1074: * ----------------------------------------------
1075: * Un symbole des minutes a été trouvé au lieu du symbole des degrés attendu.
1076: * On fera donc la modification dans les variables 'degrés' et 'minutes'. Ces
1077: * minutes sont peut-être suivies des secondes. On tentera donc de lire le
1078: * prochain nombre.
1079: */
1080: case MINUTES_FIELD: {
1081: if (swapDM) {
1082: minutes = degrees;
1083: degrees = Double.NaN;
1084: }
1085: final int indexStartField = index = pos.getIndex();
1086: while (index < length
1087: && Character.isSpaceChar(source.charAt(index))) {
1088: index++;
1089: }
1090: if (!spaceAsSeparator && index != indexStartField) {
1091: break BigBoss;
1092: }
1093: pos.setIndex(index);
1094: fieldObject = numberFormat.parse(source, pos);
1095: if (fieldObject == null) {
1096: pos.setIndex(indexStartField);
1097: break;
1098: }
1099: indexEndField = pos.getIndex();
1100: secondes = fieldObject.doubleValue();
1101: switch (skipSuffix(source, pos,
1102: (width2 != 0) ? MINUTES_FIELD : PREFIX_FIELD)) {
1103: /* -------------------------------------------------
1104: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES SECONDES
1105: * -------------------------------------------------
1106: * Un symbole des secondes explicite a été trouvée.
1107: * La lecture est donc terminée.
1108: */
1109: case SECONDS_FIELD: {
1110: break;
1111: }
1112: /* -------------------------------------------------
1113: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES SECONDES
1114: * -------------------------------------------------
1115: * Aucun symbole n'a été trouvée. Attendait-on des secondes? Si oui, les
1116: * secondes seront acceptées. Sinon, elles seront retournées au buffer.
1117: */
1118: default: {
1119: if (width2 != 0)
1120: break;
1121: // fall through
1122: }
1123: /* -------------------------------------------------
1124: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES SECONDES
1125: * -------------------------------------------------
1126: * Au lieu des degrés, on a trouvé un symbole des minutes ou des
1127: * secondes. On renvoie donc le nombre et son symbole dans le buffer.
1128: */
1129: case MINUTES_FIELD:
1130: case DEGREES_FIELD: {
1131: pos.setIndex(indexStartField);
1132: secondes = Double.NaN;
1133: break;
1134: }
1135: /* -------------------------------------------------
1136: * ANALYSE DU SYMBOLE SUIVANT LES PRÉSUMÉES SECONDES
1137: * -------------------------------------------------
1138: * Après les secondes (qu'on accepte), on a trouvé le préfix du prochain
1139: * angle à lire. On retourne ce préfix dans le buffer et on considère la
1140: * lecture terminée.
1141: */
1142: case PREFIX_FIELD: {
1143: pos.setIndex(indexEndField);
1144: break BigBoss;
1145: }
1146: }
1147: break;
1148: }
1149: }
1150: }
1151: ////////////////////////////////////////////////////////////////////
1152: // BLOC B: Prend en compte l'éventualité ou le séparateur décimal //
1153: // aurrait été absent, puis calcule l'angle en degrés. //
1154: ////////////////////////////////////////////////////////////////////
1155: if (minutes < 0) {
1156: secondes = -secondes;
1157: }
1158: if (degrees < 0) {
1159: minutes = -minutes;
1160: secondes = -secondes;
1161: }
1162: if (!decimalSeparator) {
1163: final double facteur = XMath.pow10(widthDecimal);
1164: if (width2 != 0) {
1165: if (suffix1 == null && Double.isNaN(secondes)) {
1166: if (suffix0 == null && Double.isNaN(minutes)) {
1167: degrees /= facteur;
1168: } else {
1169: minutes /= facteur;
1170: }
1171: } else {
1172: secondes /= facteur;
1173: }
1174: } else if (Double.isNaN(secondes)) {
1175: if (width1 != 0) {
1176: if (suffix0 == null && Double.isNaN(minutes)) {
1177: degrees /= facteur;
1178: } else {
1179: minutes /= facteur;
1180: }
1181: } else if (Double.isNaN(minutes)) {
1182: degrees /= facteur;
1183: }
1184: }
1185: }
1186: /*
1187: * S'il n'y a rien qui permet de séparer les degrés des minutes (par exemple si
1188: * le patron est "DDDMMmmm"), alors la variable 'degrés' englobe à la fois les
1189: * degrés, les minutes et d'éventuelles secondes. On applique une correction ici.
1190: */
1191: if (suffix1 == null && width2 != 0 && Double.isNaN(secondes)) {
1192: double facteur = XMath.pow10(width2);
1193: if (suffix0 == null && width1 != 0 && Double.isNaN(minutes)) {
1194: ///////////////////
1195: //// DDDMMSS.s ////
1196: ///////////////////
1197: secondes = degrees;
1198: minutes = (int) (degrees / facteur); // Arrondie vers 0
1199: secondes -= minutes * facteur;
1200: facteur = XMath.pow10(width1);
1201: degrees = (int) (minutes / facteur); // Arrondie vers 0
1202: minutes -= degrees * facteur;
1203: } else {
1204: ////////////////////
1205: //// DDD°MMSS.s ////
1206: ////////////////////
1207: secondes = minutes;
1208: minutes = (int) (minutes / facteur); // Arrondie vers 0
1209: secondes -= minutes * facteur;
1210: }
1211: } else if (suffix0 == null && width1 != 0
1212: && Double.isNaN(minutes)) {
1213: /////////////////
1214: //// DDDMM.m ////
1215: /////////////////
1216: final double facteur = XMath.pow10(width1);
1217: minutes = degrees;
1218: degrees = (int) (degrees / facteur); // Arrondie vers 0
1219: minutes -= degrees * facteur;
1220: }
1221: pos.setErrorIndex(-1);
1222: if (Double.isNaN(degrees))
1223: degrees = 0;
1224: if (!Double.isNaN(minutes))
1225: degrees += minutes / 60;
1226: if (!Double.isNaN(secondes))
1227: degrees += secondes / 3600;
1228: /////////////////////////////////////////////////////
1229: // BLOC C: Vérifie maintenant si l'angle ne serait //
1230: // pas suivit d'un symbole N, S, E ou W. //
1231: /////////////////////////////////////////////////////
1232: for (int index = pos.getIndex(); index < length; index++) {
1233: final char c = source.charAt(index);
1234: switch (Character.toUpperCase(c)) {
1235: case NORTH:
1236: pos.setIndex(index + 1);
1237: return new Latitude(degrees);
1238: case SOUTH:
1239: pos.setIndex(index + 1);
1240: return new Latitude(-degrees);
1241: case EAST:
1242: pos.setIndex(index + 1);
1243: return new Longitude(degrees);
1244: case WEST:
1245: pos.setIndex(index + 1);
1246: return new Longitude(-degrees);
1247: }
1248: if (!Character.isSpaceChar(c)) {
1249: break;
1250: }
1251: }
1252: return new Angle(degrees);
1253: }
1254:
1255: /**
1256: * Parse a string as an angle.
1257: *
1258: * @param source The string to parse.
1259: * @return The parsed string as an {@link Angle}, {@link Latitude}
1260: * or {@link Longitude} object.
1261: * @throws ParseException if the string has not been fully parsed.
1262: */
1263: public Angle parse(final String source) throws ParseException {
1264: final ParsePosition pos = new ParsePosition(0);
1265: final Angle ang = parse(source, pos, true);
1266: checkComplete(source, pos, false);
1267: return ang;
1268: }
1269:
1270: /**
1271: * Parse a substring as an angle. Default implementation invokes
1272: * {@link #parse(String, ParsePosition)}.
1273: *
1274: * @param source A String whose beginning should be parsed.
1275: * @param pos Position where to start parsing.
1276: * @return The parsed string as an {@link Angle},
1277: * {@link Latitude} or {@link Longitude} object.
1278: */
1279: public Object parseObject(final String source,
1280: final ParsePosition pos) {
1281: return parse(source, pos);
1282: }
1283:
1284: /**
1285: * Parse a string as an object. Default implementation invokes
1286: * {@link #parse(String)}.
1287: *
1288: * @param source The string to parse.
1289: * @return The parsed string as an {@link Angle}, {@link Latitude} or
1290: * {@link Longitude} object.
1291: * @throws ParseException if the string has not been fully parsed.
1292: */
1293: public Object parseObject(final String source)
1294: throws ParseException {
1295: return parse(source);
1296: }
1297:
1298: /**
1299: * Interprète une chaîne de caractères qui devrait représenter un nombre.
1300: * Cette méthode est utile pour lire une altitude après les angles.
1301: *
1302: * @param source Chaîne de caractères à interpréter.
1303: * @param pos Position à partir d'où commencer l'interprétation
1304: * de la chaîne {@code source}.
1305: * @return Le nombre lu comme objet {@link Number}.
1306: */
1307: final Number parseNumber(final String source,
1308: final ParsePosition pos) {
1309: return numberFormat.parse(source, pos);
1310: }
1311:
1312: /**
1313: * Vérifie si l'interprétation d'une chaîne de caractères a été complète.
1314: * Si ce n'était pas le cas, lance une exception avec un message d'erreur
1315: * soulignant les caractères problématiques.
1316: *
1317: * @param source Chaîne de caractères qui était à interpréter.
1318: * @param pos Position à laquelle s'est terminée l'interprétation de la
1319: * chaîne {@code source}.
1320: * @param isCoordinate {@code false} si on interprétait un angle,
1321: * ou {@code true} si on interprétait une coordonnée.
1322: * @throws ParseException Si la chaîne {@code source} n'a pas été
1323: * interprétée dans sa totalité.
1324: */
1325: static void checkComplete(final String source,
1326: final ParsePosition pos, final boolean isCoordinate)
1327: throws ParseException {
1328: final int length = source.length();
1329: final int origin = pos.getIndex();
1330: for (int index = origin; index < length; index++) {
1331: if (!Character.isWhitespace(source.charAt(index))) {
1332: index = pos.getErrorIndex();
1333: if (index < 0)
1334: index = origin;
1335: int lower = index;
1336: while (lower < length
1337: && Character.isWhitespace(source.charAt(lower))) {
1338: lower++;
1339: }
1340: int upper = lower;
1341: while (upper < length
1342: && !Character
1343: .isWhitespace(source.charAt(upper))) {
1344: upper++;
1345: }
1346: throw new ParseException(Errors.format(
1347: ErrorKeys.UNPARSABLE_STRING_$2, source, source
1348: .substring(lower, Math.min(lower + 10,
1349: upper))), index);
1350: }
1351: }
1352: }
1353:
1354: /**
1355: * Returns a "hash value" for this object.
1356: */
1357: public synchronized int hashCode() {
1358: int c = 78236951;
1359: if (decimalSeparator)
1360: c ^= 0xFF;
1361: if (prefix != null)
1362: c ^= prefix.hashCode();
1363: if (suffix0 != null)
1364: c = c * 37 + suffix0.hashCode();
1365: if (suffix1 != null)
1366: c ^= c * 37 + suffix1.hashCode();
1367: if (suffix2 != null)
1368: c ^= c * 37 + suffix2.hashCode();
1369: return c ^ (((((width0 << 8) ^ width1) << 8) ^ width2) << 8)
1370: ^ widthDecimal;
1371: }
1372:
1373: /**
1374: * Compare this format with the specified object for equality.
1375: */
1376: public synchronized boolean equals(final Object obj) {
1377: // On ne peut pas synchroniser "obj" si on ne veut
1378: // pas risquer un "deadlock". Voir RFE #4210659.
1379: if (obj == this ) {
1380: return true;
1381: }
1382: if (obj != null && getClass().equals(obj.getClass())) {
1383: final AngleFormat cast = (AngleFormat) obj;
1384: return width0 == cast.width0
1385: && width1 == cast.width1
1386: && width2 == cast.width2
1387: && widthDecimal == cast.widthDecimal
1388: && decimalSeparator == cast.decimalSeparator
1389: && Utilities.equals(prefix, cast.prefix)
1390: && Utilities.equals(suffix0, cast.suffix0)
1391: && Utilities.equals(suffix1, cast.suffix1)
1392: && Utilities.equals(suffix2, cast.suffix2)
1393: && Utilities
1394: .equals(numberFormat
1395: .getDecimalFormatSymbols(),
1396: cast.numberFormat
1397: .getDecimalFormatSymbols());
1398: } else {
1399: return false;
1400: }
1401: }
1402:
1403: /**
1404: * Returns a string representation of this object.
1405: */
1406: public String toString() {
1407: return Utilities.getShortClassName(this ) + '[' + toPattern()
1408: + ']';
1409: }
1410: }
|