Package net.sourceforge.gpstools.utils

Source Code of net.sourceforge.gpstools.utils.AngleFormat$Utilities

/*
*    gpsdings
*    This class is derived from org.geotools.measure.AngleFormat
*    GeoTools - OpenSource mapping toolkit
*    http://geotools.org
*    (C) 2007, Moritz Ringler
*    (C) 2003-2006, GeoTools Project Managment Committee (PMC)
*    (C) 2001, Institut de Recherche pour le D\u00e9veloppement
*    (C) 1999, Fisheries and Oceans Canada
*
*    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.measure;
package net.sourceforge.gpstools.utils;

// J2SE dependencies
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Locale;

// Geotools dependencies
//import org.geotools.resources.Utilities;
//import org.geotools.resources.XMath;
//import org.geotools.resources.i18n.Errors;
//import org.geotools.resources.i18n.ErrorKeys;


/**
* Parse and format angle according a specified pattern. The pattern is a string
* containing any characters, with a special meaning for the following characters:
*
* <blockquote><table cellpadding="3">
*     <tr><td>{@code D}</td><td>&nbsp;&nbsp;The integer part of degrees</td></tr>
*     <tr><td>{@code d}</td><td>&nbsp;&nbsp;The fractional part of degrees</td></tr>
*     <tr><td>{@code M}</td><td>&nbsp;&nbsp;The integer part of minutes</td></tr>
*     <tr><td>{@code m}</td><td>&nbsp;&nbsp;The fractional part of minutes</td></tr>
*     <tr><td>{@code S}</td><td>&nbsp;&nbsp;The integer part of seconds</td></tr>
*     <tr><td>{@code s}</td><td>&nbsp;&nbsp;The fractional part of seconds</td></tr>
*     <tr><td>{@code .}</td><td>&nbsp;&nbsp;The decimal separator</td></tr>
* </table></blockquote>
* <br>
* Upper-case letters {@code D}, {@code M} and {@code S} are for the integer
* parts of degrees, minutes and seconds respectively. They must appear in this order (e.g.
* "<code>M'D</code>" is illegal because "M" and "S" are inverted; "<code>D\u00b0S</code>" is
* illegal too because there is no "M" between "D" and "S"). Lower-case letters {@code d},
* {@code m} and {@code s} are for fractional parts of degrees, minutes and seconds
* respectively. Only one of those may appears in a pattern, and it must be the last special
* symbol (e.g. "<code>D.dd\u00b0MM'</code>" is illegal because "d" is followed by "M";
* "{@code D.mm}" is illegal because "m" is not the fractional part of "D").
* <br><br>
* The number of occurrence of {@code D}, {@code M}, {@code S} and their
* lower-case counterpart is the number of digits to format. For example, "DD.ddd" will
* format angle with two digits for the integer part and three digits for the fractional
* part (e.g. 4.4578 will be formatted as "04.458"). Separator characters like <code>\u00b0</code>,
* <code>'</code> and <code>"</code> and inserted "as-is" in the formatted string (except the
* decimal separator dot ("{@code .}"), which is replaced by the local-dependent decimal
* separator). Separator characters may be completely omitted; {@code AngleFormat} will
* still differentiate degrees, minutes and seconds fields according the pattern. For example,
* "{@code 0480439}" with the pattern "{@code DDDMMmm}" will be parsed as 48\u00b004.39'.
* <br><br>
* The following table gives some examples of legal patterns.
*
* <blockquote><table cellpadding="3">
* <tr><th>Pattern                </th>  <th>Example   </th></tr>
* <tr><td><code>DD\u00b0MM'SS" </code></td>  <td>48\u00b030'00" </td></tr>
* <tr><td><code>DD\u00b0MM'    </code></td>  <td>48\u00b030'    </td></tr>
* <tr><td>{@code DD.ddd    }</td>  <td>48.500    </td></tr>
* <tr><td>{@code DDMM      }</td>  <td>4830      </td></tr>
* <tr><td>{@code DDMMSS    }</td>  <td>483000    </td></tr>
* </table></blockquote>
*
* @see Angle
* @see Latitude
* @see Longitude
*
* @since 2.0
* @source $URL: http://svn.geotools.org/geotools/tags/2.4-M4/modules/library/referencing/src/main/java/org/geotools/measure/AngleFormat.java $
* @version $Id: AngleFormat.java 441 2010-12-13 20:04:20Z ringler $
* @author Martin Desruisseaux
*/
public class AngleFormat extends Format {
    /**
   *
   */
  private static final long serialVersionUID = 6923178789938161893L;

  /**
     * Caract\u00e8re repr\u00e9sentant l'h\u00e9misph\u00e8re nord.
     * Il doit obligatoirement \u00eatre en majuscule.
     */
    final private char NORTH;

    /**
     * Caract\u00e8re repr\u00e9sentant l'h\u00e9misph\u00e8re sud.
     * Il doit obligatoirement \u00eatre en majuscule.
     */
    final private char SOUTH;

    /**
     * Caract\u00e8re repr\u00e9sentant l'h\u00e9misph\u00e8re est.
     * Il doit obligatoirement \u00eatre en majuscule.
     */
    final private char EAST;

    /**
     * Caract\u00e8re repr\u00e9sentant l'h\u00e9misph\u00e8re ouest.
     * Il doit obligatoirement \u00eatre en majuscule.
     */
    final private char WEST;

    /**
     * Constante indique que l'angle
     * \u00e0 formater est une longitude.
     */
    static final int LONGITUDE=0;

    /**
     * Constante indique que l'angle
     * \u00e0 formater est une latitude.
     */
    static final int LATITUDE=1;

    /**
     * Constante indique que le nombre
     * \u00e0 formater est une altitude.
     */
    static final int ALTITUDE=2;

    /**
     * A constant for the symbol to appears before the degrees fields.
     * Fields PREFIX, DEGREES, MINUTES and SECONDS <strong>must</strong>
     * have increasing values (-1, 0, +1, +2, +3).
     */
    private static final int PREFIX_FIELD = -1;

    /**
     * Constant for degrees field. When formatting a string, this value may be
     * specified to the {@link java.text.FieldPosition} constructor in order to
     * get the bounding index where degrees have been written.
     */
    public static final int DEGREES_FIELD = 0;

    /**
     * Constant for minutes field. When formatting a string, this value may be
     * specified to the {@link java.text.FieldPosition} constructor in order to
     * get the bounding index where minutes have been written.
     */
    public static final int MINUTES_FIELD = 1;

    /**
     * Constant for seconds field. When formatting a string, this value may be
     * specified to the {@link java.text.FieldPosition} constructor in order to
     * get the bounding index where seconds have been written.
     */
    public static final int SECONDS_FIELD = 2;

    /**
     * Constant for hemisphere field. When formatting a string, this value may be
     * specified to the {@link java.text.FieldPosition} constructor in order to
     * get the bounding index where the hemisphere synbol has been written.
     */
    public static final int HEMISPHERE_FIELD = 3;

    /**
     * Symboles repr\u00e9sentant les degr\u00e9s (0),
     * minutes (1) et les secondes (2).
     */
    private static final char[] SYMBOLS = {'D', 'M', 'S'};

    /**
     * Nombre minimal d'espaces que doivent occuper les parties
     * enti\u00e8res des degr\u00e9s (0), minutes (1) et secondes (2). Le
     * champs {@code widthDecimal} indique la largeur fixe
     * que doit avoir la partie d\u00e9cimale. Il s'appliquera au
     * dernier champs non-zero dans {@code width0..2}.
     */
    private int width0=1, width1=2, width2=0, widthDecimal=0;

    /**
     * Caract\u00e8res \u00e0 ins\u00e9rer au d\u00e9but ({@code prefix}) et \u00e0 la
     * suite des degr\u00e9s, minutes et secondes ({@code suffix0..2}).
     * Ces champs doivent \u00eatre {@code null} s'il n'y a rien \u00e0 ins\u00e9rer.
     */
    private String prefix=null, suffix0="\u00B0", suffix1="'", suffix2="\"";

    /**
     * Indique s'il faut utiliser le s\u00e9parateur d\u00e9cimal pour s\u00e9parer la partie
     * enti\u00e8re de la partie fractionnaire. La valeur {@code false} indique
     * que les parties enti\u00e8res et fractionnaires doivent \u00eatre \u00e9crites ensembles
     * (par exemple 34867 pour 34.867). La valeur par d\u00e9faut est {@code true}.
     */
    private boolean decimalSeparator=true;

    /**
     * Format \u00e0 utiliser pour \u00e9crire les nombres
     * (degr\u00e9s, minutes ou secondes) \u00e0 l'int\u00e9rieur
     * de l'\u00e9criture d'un angle.
     */
    private final DecimalFormat numberFormat;

    /**
     * Objet \u00e0 transmetre aux m\u00e9thodes {@code DecimalFormat.format}.
     * Ce param\u00e8tre existe simplement pour \u00e9viter de cr\u00e9er cet objet trop
     * souvent, alors qu'on ne s'y int\u00e9resse pas.
     */
    private transient FieldPosition dummy = new FieldPosition(0);

    /**
     * Restore fields after deserialization.
     */
    private void readObject(final ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        in.defaultReadObject();
        dummy = new FieldPosition(0);
    }

    /**
     * Returns the width of the specified field.
     */
    private int getWidth(final int index) {
        switch (index) {
            case DEGREES_FIELD:  return width0;
            case MINUTES_FIELD:  return width1;
            case SECONDS_FIELD:  return width2;
            default:             return 0; // Must be 0 (important!)
        }
    }

    /**
     * Set the width for the specified field.
     * All folowing fields will be set to 0.
     */
    @SuppressWarnings("fallthrough")
    private void setWidth(final int index, int width) {
        switch (index) {
            case DEGREES_FIELD: width0=width; width=0; // fall through
            case MINUTES_FIELD: width1=width; width=0; // fall through
            case SECONDS_FIELD: width2=width;          // fall through
        }
    }

    /**
     * Returns the suffix for the specified field.
     */
    private String getSuffix(final int index) {
        switch (index) {
            case  PREFIX_FIELD: return prefix;
            case DEGREES_FIELD: return suffix0;
            case MINUTES_FIELD: return suffix1;
            case SECONDS_FIELD: return suffix2;
            default:            return null;
        }
    }

    /**
     * Set the suffix for the specified field. Suffix
     * for all following fields will be set to their
     * default value.
     */
    @SuppressWarnings("fallthrough")
    private void setSuffix(final int index, String s) {
        switch (index) {
            case  PREFIX_FIELD:  prefix=s; s="\u00B0"// fall through
            case DEGREES_FIELD: suffix0=s; s="'";       // fall through
            case MINUTES_FIELD: suffix1=s; s="\"";      // fall through
            case SECONDS_FIELD: suffix2=s;              // fall through
        }
    }

    /**
     * Construct a new {@code AngleFormat} for the specified locale.
     */
    public static AngleFormat getInstance(final Locale locale) {
        return new AngleFormat("D\u00B0MM.m'", locale);
    }

    /**
     * Construct a new {@code AngleFormat} using
     * the current default locale and a default pattern.
     */
    public AngleFormat() {
        this("D\u00B0MM.m'");
    }

    /**
     * Construct a new {@code AngleFormat} using the
     * current default locale and the specified pattern.
     *
     * @param  pattern Pattern to use for parsing and formatting angle.
     *         See class description for an explanation of how this pattern work.
     * @throws IllegalArgumentException If the specified pattern is not legal.
     */
    public AngleFormat(final String pattern) throws IllegalArgumentException {
        this(pattern, new DecimalFormatSymbols());
    }

    /**
     * Construct a new {@code AngleFormat}
     * using the specified pattern and locale.
     *
     * @param  pattern Pattern to use for parsing and formatting angle.
     *         See class description for an explanation of how this pattern work.
     * @param  locale Locale to use.
     * @throws IllegalArgumentException If the specified pattern is not legal.
     */
    public AngleFormat(final String pattern, final Locale locale) throws IllegalArgumentException {
        this(pattern, new DecimalFormatSymbols(locale));
    }

    /**
     * Construct a new {@code AngleFormat}
     * using the specified pattern and decimal symbols.
     *
     * @param  pattern Pattern to use for parsing and formatting angle.
     *         See class description for an explanation of how this pattern work.
     * @param  symbols The symbols to use for parsing and formatting numbers.
     * @throws IllegalArgumentException If the specified pattern is not legal.
     */
    public AngleFormat(final String pattern, final DecimalFormatSymbols symbols) {
        // NOTE: pour cette routine, il ne faut PAS que DecimalFormat
        //       reconnaisse la notation exponentielle, parce que \u00e7a
        //       risquerait d'\u00eatre confondu avec le "E" de "Est".
        this(pattern, symbols, "NWSE".toCharArray());
    }

    public AngleFormat(final String pattern, final DecimalFormatSymbols symbols, char[] nwse) {
        // NOTE: pour cette routine, il ne faut PAS que DecimalFormat
        //       reconnaisse la notation exponentielle, parce que \u00e7a
        //       risquerait d'\u00eatre confondu avec le "E" de "Est".
        numberFormat=new DecimalFormat("#0", symbols);
        applyPattern(pattern);
        if(nwse.length != 4){
            throw new IllegalArgumentException("nwse must have length 4.");
        }
        NORTH = Character.toUpperCase(nwse[0]);
        WEST = Character.toUpperCase(nwse[1]);
        SOUTH = Character.toUpperCase(nwse[2]);
        EAST = Character.toUpperCase(nwse[3]);
    }

    /**
     * Set the pattern to use for parsing and formatting angle.
     * See class description for an explanation of how patterns work.
     *
     * @param  pattern Pattern to use for parsing and formatting angle.
     * @throws IllegalArgumentException If the specified pattern is not legal.
     */
    @SuppressWarnings("fallthrough")
    public synchronized void applyPattern(final String pattern) throws IllegalArgumentException {
        widthDecimal = 0;
        decimalSeparator = true;
        int startPrefix = 0;
        int symbolIndex = 0;
        boolean parseFinished = false;
        final int length = pattern.length();
        for (int i=0; i<length; i++) {
            /*
             * On examine un \u00e0 un tous les caract\u00e8res du patron en
             * sautant ceux qui ne sont pas r\u00e9serv\u00e9s ("D", "M", "S"
             * et leur \u00e9quivalents en minuscules). Les caract\u00e8res
             * non-reserv\u00e9s seront m\u00e9moris\u00e9s comme suffix plus tard.
             */
            final char c = pattern.charAt(i);
            final char upperCaseC = Character.toUpperCase(c);
            for (int field=DEGREES_FIELD; field<SYMBOLS.length; field++) {
                if (upperCaseC == SYMBOLS[field]) {
                    /*
                     * Un caract\u00e8re r\u00e9serv\u00e9 a \u00e9t\u00e9 trouv\u00e9. V\u00e9rifie maintenant
                     * s'il est valide. Par exemple il serait illegal d'avoir
                     * comme patron "MM.mm" sans qu'il soit pr\u00e9c\u00e9d\u00e9 des degr\u00e9s.
                     * On attend les lettres "D", "M" et "S" dans l'ordre. Si
                     * le caract\u00e8re est en lettres minuscules, il doit \u00eatre le
                     * m\u00eame que le dernier code (par exemple "DD.mm" est illegal).
                     */
                    if (c==upperCaseC) {
                        symbolIndex++;
                    }
                    if (field!=symbolIndex-1 || parseFinished) {
                        setWidth(DEGREES_FIELD, 1);
                        setSuffix(PREFIX_FIELD, null);
                        widthDecimal=0;
                        decimalSeparator=true;
                        throw new IllegalArgumentException("Illegal angle pattern: " + pattern);
                    }
                    if (c==upperCaseC) {
                        /*
                         * M\u00e9morise les caract\u00e8res qui pr\u00e9c\u00e9daient ce code comme suffix
                         * du champs pr\u00e9c\u00e9dent. Puis on comptera le nombre de fois que le
                         * code se r\u00e9p\u00e8te, en m\u00e9morisant cette information comme largeur
                         * de ce champ.
                         */
                        setSuffix(field-1, (i>startPrefix) ? pattern.substring(startPrefix, i) : null);
                        int w=1; while (++i<length && pattern.charAt(i)==c) w++;
                        setWidth(field, w);
                    } else {
                        /*
                         * Si le caract\u00e8re est une minuscule, ce qui le pr\u00e9c\u00e9dait sera le
                         * s\u00e9parateur d\u00e9cimal plut\u00f4t qu'un suffix. On comptera le nombre
                         * d'occurences du caract\u00e8res pour obtenir la pr\u00e9cision.
                         */
                        switch (i-startPrefix) {
                            case 0: decimalSeparator=false; break;
                            case 1: if (pattern.charAt(startPrefix)=='.') {
                                decimalSeparator=true;
                                break;
                            }
                            default: throw new IllegalArgumentException("Illegal angle pattern: " + pattern);
                        }
                        int w=1; while (++i<length && pattern.charAt(i)==c) w++;
                        widthDecimal=w;
                        parseFinished=true;
                    }
                    startPrefix = i--;
                    break; // Break 'j' and continue 'i'.
                }
            }
        }
        setSuffix(symbolIndex-1, (startPrefix<length) ? pattern.substring(startPrefix) : null);
    }

    /**
     * Returns the pattern used for parsing and formatting angles.
     * See class description for an explanation of how patterns work.
     */
    public synchronized String toPattern() {
        char symbol='#';
        final StringBuffer buffer=new StringBuffer();
        for (int field=DEGREES_FIELD; field<=SYMBOLS.length; field++) {
            final String previousSuffix=getSuffix(field-1);
            int w=getWidth(field);
            if (w>0) {
                /*
                 * Proc\u00e8de \u00e0 l'\u00e9criture de la partie enti\u00e8re des degr\u00e9s,
                 * minutes ou secondes. Le suffix du champs pr\u00e9c\u00e9dent
                 * sera \u00e9crit avant les degr\u00e9s, minutes ou secondes.
                 */
                if (previousSuffix!=null) {
                    buffer.append(previousSuffix);
                }
                symbol=SYMBOLS[field];
                do buffer.append(symbol);
                while (--w>0);
            } else {
                /*
                 * Proc\u00e8de \u00e0 l'\u00e9criture de la partie d\u00e9cimale des
                 * degr\u00e9s, minutes ou secondes. Le suffix du ce
                 * champs sera \u00e9crit apr\u00e8s cette partie fractionnaire.
                 */
                w=widthDecimal;
                if (w>0) {
                    if (decimalSeparator) buffer.append('.');
                    symbol=Character.toLowerCase(symbol);
                    do buffer.append(symbol);
                    while (--w>0);
                }
                if (previousSuffix!=null) {
                    buffer.append(previousSuffix);
                }
                break;
            }
        }
        return buffer.toString();
    }

    /**
     * Format an angle. The string will be formatted according
     * the pattern set in the last call to {@link #applyPattern}.
     *
     * @param  angle Angle to format, in degrees.
     * @return The formatted string.
     */
    public final String format(final double angle) {
        return format(angle, new StringBuffer(), null).toString();
    }

    /**
     * Formats an angle and appends the resulting text to a given string buffer.
     * The string will be formatted according the pattern set in the last call
     * to {@link #applyPattern}.
     *
     * @param  angle      Angle to format, in degrees.
     * @param  toAppendTo Where the text is to be appended.
     * @param  pos        An optional {@link FieldPosition} identifying a field
     *                    in the formatted text, or {@code null} if this
     *                    information is not wanted. This field position shall
     *                    be constructed with one of the following constants:
     *                    {@link #DEGREES_FIELD},
     *                    {@link #MINUTES_FIELD},
     *                    {@link #SECONDS_FIELD} or
     *                    {@link #HEMISPHERE_FIELD}.
     *
     * @return The string buffer passed in as {@code toAppendTo}, with formatted text appended.
     */
    public synchronized StringBuffer format(final double angle,
                                            StringBuffer toAppendTo,
                                            final FieldPosition pos)
    {
        double degrees = angle;
        /*
         * Calcule \u00e0 l'avance les minutes et les secondes. Si les minutes et secondes
         * ne doivent pas \u00eatre \u00e9crits, on m\u00e9morisera NaN. Notez que pour extraire les
         * parties enti\u00e8res, on utilise (int) au lieu de 'Math.floor' car (int) arrondie
         * vers 0 (ce qui est le comportement souhait\u00e9) alors que 'floor' arrondie vers
         * l'entier inf\u00e9rieur.
         */
        double minutes  = Double.NaN;
        double secondes = Double.NaN;
        if (width1!=0 && !Double.isNaN(angle)) {
            int tmp = (int) degrees; // Arrondie vers 0 m\u00eame si n\u00e9gatif.
            minutes = Math.abs(degrees-tmp)*60;
            degrees = tmp;
            if (minutes<0 || minutes>60) {
                // Erreur d'arrondissement (parce que l'angle est trop \u00e9lev\u00e9)
                throw new IllegalArgumentException("Angle overflow: " + angle);
            }
            if (width2 != 0) {
                tmp      = (int) minutes; // Arrondie vers 0 m\u00eame si n\u00e9gatif.
                secondes = (minutes-tmp)*60;
                minutes  = tmp;
                if (secondes<0 || secondes>60) {
                    // Erreur d'arrondissement (parce que l'angle est trop \u00e9lev\u00e9)
                    throw new IllegalArgumentException("Angle overflow: " + angle);
                }
                /*
                 * On applique maintenant une correction qui tiendra
                 * compte des probl\u00e8mes d'arrondissements.
                 */
                final double puissance=XMath.pow10(widthDecimal);
                secondes=Math.rint(secondes*puissance)/puissance;
                tmp = (int) (secondes/60);
                secondes -= 60*tmp;
                minutes += tmp;
            } else {
                final double puissance=XMath.pow10(widthDecimal);
                minutes = Math.rint(minutes*puissance)/puissance;
            }
            tmp = (int) (minutes/60); // Arrondie vers 0 m\u00eame si n\u00e9gatif.
            minutes -= 60*tmp;
            degrees += tmp;
        }
        /*
         * Les variables 'degr\u00e9s', 'minutes' et 'secondes' contiennent
         * maintenant les valeurs des champs \u00e0 \u00e9crire, en principe \u00e9pur\u00e9s
         * des probl\u00e8mes d'arrondissements. Proc\u00e8de maintenant \u00e0 l'\u00e9criture
         * de l'angle.
         */
        if (prefix != null) {
            toAppendTo.append(prefix);
        }
        final int field;
        if (pos != null) {
            field = pos.getField();
            pos.setBeginIndex(0);
            pos.setEndIndex(0);
        } else {
            field=PREFIX_FIELD;
        }
        toAppendTo = formatField(degrees, toAppendTo,
                                 field==DEGREES_FIELD ? pos : null,
                                 width0, width1==0, suffix0);
        if (!Double.isNaN(minutes)) {
            toAppendTo=formatField(minutes, toAppendTo,
                                   field==MINUTES_FIELD ? pos : null,
                                   width1, width2==0, suffix1);
        }
        if (!Double.isNaN(secondes)) {
            toAppendTo=formatField(secondes, toAppendTo,
                                   field==SECONDS_FIELD ? pos : null,
                                   width2, true, suffix2);
        }
        return toAppendTo;
    }

    /**
     * Proc\u00e8de \u00e0 l'\u00e9criture d'un champ de l'angle.
     *
     * @param value Valeur \u00e0 \u00e9crire.
     * @param toAppendTo Buffer dans lequel \u00e9crire le champs.
     * @param pos Objet dans lequel m\u00e9moriser les index des premiers
     *        et derniers caract\u00e8res \u00e9crits, ou {@code null}
     *        pour ne pas m\u00e9moriser ces index.
     * @param w Nombre de minimal caract\u00e8res de la partie enti\u00e8re.
     * @param last {@code true} si ce champs est le dernier,
     *        et qu'il faut donc \u00e9crire la partie d\u00e9cimale.
     * @param s Suffix \u00e0 \u00e9crire apr\u00e8s le nombre (peut \u00eatre nul).
     */
    private StringBuffer formatField(double value,
                                     StringBuffer toAppendTo, final FieldPosition pos,
                                     final int w, final boolean last, final String s)
    {
        final int startPosition=toAppendTo.length();
        if (!last) {
            numberFormat.setMinimumIntegerDigits(w);
            numberFormat.setMaximumFractionDigits(0);
            toAppendTo = numberFormat.format(value, toAppendTo, dummy);
        } else if (decimalSeparator) {
            numberFormat.setMinimumIntegerDigits(w);
            numberFormat.setMinimumFractionDigits(widthDecimal);
            numberFormat.setMaximumFractionDigits(widthDecimal);
            toAppendTo = numberFormat.format(value, toAppendTo, dummy);
        } else {
            value *= XMath.pow10(widthDecimal);
            numberFormat.setMaximumFractionDigits(0);
            numberFormat.setMinimumIntegerDigits(w+widthDecimal);
            toAppendTo = numberFormat.format(value, toAppendTo, dummy);
        }
        if (s!=null) {
            toAppendTo.append(s);
        }
        if (pos!=null) {
            pos.setBeginIndex(startPosition);
            pos.setEndIndex(toAppendTo.length()-1);
        }
        return toAppendTo;
    }

    /**
     * Formats an angle, a latitude or a longitude and appends the resulting text
     * to a given string buffer. The string will be formatted according the pattern
     * set in the last call to {@link #applyPattern}. The argument {@code obj}
     * shall be an {@link Angle} object or one of its derived class ({@link Latitude},
     * {@link Longitude}). If {@code obj} is a {@link Latitude} object, then a
     * symbol "N" or "S" will be appended to the end of the string (the symbol will
     * be choosen according the angle's sign). Otherwise, if {@code obj} is a
     * {@link Longitude} object, then a symbol "E" or "W" will be appended to the
     * end of the string. Otherwise, no hemisphere symbol will be appended.
     * <br><br>
     * Strictly speaking, formatting ordinary numbers is not the
     * {@code AngleFormat}'s job. Nevertheless, this method
     * accept {@link Number} objects. This capability is provided
     * only as a convenient way to format altitude numbers together
     * with longitude and latitude angles.
     *
     * @param  obj        {@link Angle} or {@link Number} object to format.
     * @param  toAppendTo Where the text is to be appended.
     * @param  pos        An optional {@link FieldPosition} identifying a field
     *                    in the formatted text, or {@code null} if this
     *                    information is not wanted. This field position shall
     *                    be constructed with one of the following constants:
     *                    {@link #DEGREES_FIELD},
     *                    {@link #MINUTES_FIELD},
     *                    {@link #SECONDS_FIELD} or
     *                    {@link #HEMISPHERE_FIELD}.
     *
     * @return The string buffer passed in as {@code toAppendTo}, with
     *         formatted text appended.
     * @throws IllegalArgumentException if {@code obj} if not an object
     *         of class {@link Angle} or {@link Number}.
     */
    @Override
    public synchronized StringBuffer format(final Object obj,
                                            StringBuffer toAppendTo,
                                            final FieldPosition pos)
        throws IllegalArgumentException
    {
        if (obj instanceof Latitude) {
            return format(((Latitude) obj).degrees(), toAppendTo, pos, NORTH, SOUTH);
        }
        if (obj instanceof Longitude) {
            return format(((Longitude) obj).degrees(), toAppendTo, pos, EAST, WEST);
        }
        if (obj instanceof Angle) {
            return format(((Angle) obj).degrees(), toAppendTo, pos);
        }
        if (obj instanceof Number) {
            numberFormat.setMinimumIntegerDigits(1);
            numberFormat.setMinimumFractionDigits(0);
            numberFormat.setMaximumFractionDigits(2);
            return numberFormat.format(obj, toAppendTo, (pos!=null) ? pos : dummy);
        }
        throw new IllegalArgumentException("Not an angle object: " +
                Utilities.getShortClassName(obj));
    }

    /**
     * Proc\u00e8de \u00e0 l'\u00e9criture d'un angle, d'une latitude ou d'une longitude.
     *
     * @param  number     Angle ou nombre \u00e0 \u00e9crire.
     * @param  type       Type de l'angle ou du nombre:
     *                    {@link #LONGITUDE},
     *                    {@link #LATITUDE} ou
     *                    {@link #ALTITUDE}.
     * @param  toAppendTo Buffer dans lequel \u00e9crire l'angle.
     * @param  pos        En entr\u00e9, le code du champs dont on d\u00e9sire les index
     *                    ({@link #DEGREES_FIELD},
     *                     {@link #MINUTES_FIELD},
     *                     {@link #SECONDS_FIELD} ou
     *                     {@link #HEMISPHERE_FIELD}).
     *                    En sortie, les index du champs demand\u00e9. Ce param\u00e8tre
     *                    peut \u00eatre nul si cette information n'est pas d\u00e9sir\u00e9e.
     *
     * @return Le buffer {@code toAppendTo} par commodit\u00e9.
     */
    synchronized StringBuffer format(final double number, final int type,
                                     StringBuffer toAppendTo,
                                     final FieldPosition pos)
    {
        switch (type) {
            default:        throw new IllegalArgumentException(Integer.toString(type)); // Should not happen.
            case LATITUDE:  return format(number, toAppendTo, pos, NORTH, SOUTH);
            case LONGITUDE: return format(number, toAppendTo, pos, EAST,  WEST );
            case ALTITUDE: {
                numberFormat.setMinimumIntegerDigits(1);
                numberFormat.setMinimumFractionDigits(0);
                numberFormat.setMaximumFractionDigits(2);
                return numberFormat.format(number, toAppendTo, (pos!=null) ? pos : dummy);
            }
        }
    }

    /**
     * Proc\u00e8de \u00e0 l'\u00e9criture d'un angle suivit d'un suffix 'N','S','E' ou 'W'.
     * L'angle sera format\u00e9 en utilisant comme mod\u00e8le le patron sp\u00e9cifi\u00e9 lors
     * du dernier appel de la m\u00e9thode {@link #applyPattern}.
     *
     * @param  angle      Angle \u00e0 \u00e9crire, en degr\u00e9s.
     * @param  toAppendTo Buffer dans lequel \u00e9crire l'angle.
     * @param  pos        En entr\u00e9, le code du champs dont on d\u00e9sire les index
     *                    ({@link #DEGREES_FIELD},
     *                     {@link #MINUTES_FIELD},
     *                     {@link #SECONDS_FIELD} ou
     *                     {@link #HEMISPHERE_FIELD}).
     *                    En sortie, les index du champs demand\u00e9. Ce param\u00e8tre
     *                    peut \u00eatre nul si cette information n'est pas d\u00e9sir\u00e9e.
     * @param north       Caract\u00e8res \u00e0 \u00e9crire si l'angle est positif ou nul.
     * @param south       Caract\u00e8res \u00e0 \u00e9crire si l'angle est n\u00e9gatif.
     *
     * @return Le buffer {@code toAppendTo} par commodit\u00e9.
     */
    private StringBuffer format(final double angle,
                                StringBuffer toAppendTo,
                                final FieldPosition pos,
                                final char north, final char south)
    {
        toAppendTo = format(Math.abs(angle), toAppendTo, pos);
        final int start = toAppendTo.length();
        toAppendTo.append(angle<0 ? south : north);
        if (pos!=null && pos.getField()==HEMISPHERE_FIELD) {
            pos.setBeginIndex(start);
            pos.setEndIndex(toAppendTo.length()-1);
        }
        return toAppendTo;
    }

    /**
     * Ignore le suffix d'un nombre. Cette m\u00e9thode est appell\u00e9e par la m\u00e9thode
     * {@link #parse} pour savoir quel champs il vient de lire. Par exemple si
     * l'on vient de lire les degr\u00e9s dans "48\u00b012'", alors cette m\u00e9thode extraira
     * le "\u00b0" et retournera 0 pour indiquer que l'on vient de lire des degr\u00e9s.
     *
     * Cette m\u00e9thode se chargera d'ignorer les espaces qui pr\u00e9c\u00e8dent le suffix.
     * Elle tentera ensuite de d'abord interpr\u00e9ter le suffix selon les symboles
     * du patron (sp\u00e9cifi\u00e9 avec {@link #applyPattern}. Si le suffix n'a pas \u00e9t\u00e9
     * reconnus, elle tentera ensuite de le comparer aux symboles standards
     * (\u00b0 ' ").
     *
     * @param  source Cha\u00eene dans laquelle on doit sauter le suffix.
     * @param  pos En entr\u00e9, l'index du premier caract\u00e8re \u00e0 consid\u00e9rer dans la
     *         cha\u00eene {@code pos}. En sortie, l'index du premier caract\u00e8re
     *         suivant le suffix (c'est-\u00e0-dire index \u00e0 partir d'o\u00f9 continuer la
     *         lecture apr\u00e8s l'appel de cette m\u00e9thode). Si le suffix n'a pas \u00e9t\u00e9
     *         reconnu, alors cette m\u00e9thode retourne par convention {@code SYMBOLS.length}.
     * @param  field Champs \u00e0 v\u00e9rifier de pr\u00e9f\u00e9rences. Par exemple la valeur 1 signifie que les
     *         suffix des minutes et des secondes devront \u00eatre v\u00e9rifi\u00e9s avant celui des degr\u00e9s.
     * @return Le num\u00e9ro du champs correspondant au suffix qui vient d'\u00eatre extrait:
     *         -1 pour le pr\u00e9fix de l'angle, 0 pour le suffix des degr\u00e9s, 1 pour le
     *         suffix des minutes et 2 pour le suffix des secondes. Si le texte n'a
     *         pas \u00e9t\u00e9 reconnu, retourne {@code SYMBOLS.length}.
     */
    private int skipSuffix(final String source, final ParsePosition pos, int field) {
        /*
         * Essaie d'abord de sauter les suffix qui
         * avaient \u00e9t\u00e9 sp\u00e9cifi\u00e9s dans le patron.
         */
        final int length=source.length();
        int start=pos.getIndex();
        for (int j=SYMBOLS.length; j>=0; j--) { // C'est bien j>=0 et non j>0.
            int index=start;
            final String toSkip=getSuffix(field);
            if (toSkip!=null) {
                final int toSkipLength=toSkip.length();
                do {
                    if (source.regionMatches(index, toSkip, 0, toSkipLength)) {
                        pos.setIndex(index+toSkipLength);
                        return field;
                    }
                }
                while (index<length && Character.isSpaceChar(source.charAt(index++)));
            }
            if (++field >= SYMBOLS.length) field=-1;
        }
        /*
         * Le texte trouv\u00e9 ne correspondant \u00e0 aucun suffix du patron,
         * essaie maintenant de sauter un des suffix standards (apr\u00e8s
         * avoir ignor\u00e9 les espaces qui le pr\u00e9c\u00e9daient).
         */
        char c;
        do {
            if (start>=length) {
                return SYMBOLS.length;
            }
        }
        while (Character.isSpaceChar(c=source.charAt(start++)));
        switch (c) {
            case '\u00B0' : pos.setIndex(start); return DEGREES_FIELD;
            case '\''     : pos.setIndex(start); return MINUTES_FIELD;
            case '"'      : pos.setIndex(start); return SECONDS_FIELD;
            default       : return SYMBOLS.length; // Unknow field.
        }
    }

    /**
     * Parse a string as an angle. This method can parse an angle even if it
     * doesn't comply exactly to the expected pattern. For example, this method
     * will parse correctly string "<code>48\u00b012.34'</code>" even if the expected
     * pattern was "{@code DDMM.mm}" (i.e. the string should have been
     * "{@code 4812.34}"). Spaces between degrees, minutes and secondes
     * are ignored. If the string ends with an hemisphere symbol "N" or "S",
     * then this method returns an object of class {@link Latitude}. Otherwise,
     * if the string ends with an hemisphere symbol "E" or "W", then this method
     * returns an object of class {@link Longitude}. Otherwise, this method
     * returns an object of class {@link Angle}.
     *
     * @param source A String whose beginning should be parsed.
     * @param pos    Position where to start parsing.
     * @return       The parsed string as an {@link Angle}, {@link Latitude}
     *               or {@link Longitude} object.
     */
    public Angle parse(final String source, final ParsePosition pos) {
        return parse(source, pos, false);
    }

    /**
     * Interpr\u00e8te une cha\u00eene de caract\u00e8res repr\u00e9sentant un angle. Les r\u00e8gles
     * d'interpr\u00e9tation de cette m\u00e9thode sont assez souples. Par exemple cettte
     * m\u00e9thode interpr\u00e9tera correctement la cha\u00eene "48\u00b012.34'" m\u00eame si le patron
     * attendu \u00e9tait "DDMM.mm" (c'est-\u00e0-dire que la cha\u00eene aurait du \u00eatre "4812.34").
     * Les espaces entre les degr\u00e9s, minutes et secondes sont accept\u00e9s. Si l'angle
     * est suivit d'un symbole "N" ou "S", alors l'objet retourn\u00e9 sera de la classe
     * {@link Latitude}. S'il est plutot suivit d'un symbole "E" ou "W", alors l'objet
     * retourn\u00e9 sera de la classe {@link Longitude}. Sinon, il sera de la classe
     * {@link Angle}.
     *
     * @param source           Cha\u00eene de caract\u00e8res \u00e0 lire.
     * @param pos              Position \u00e0 partir d'o\u00f9 interpr\u00e9ter la cha\u00eene.
     * @param spaceAsSeparator Indique si l'espace est accept\u00e9 comme s\u00e9parateur
     *                         \u00e0 l'int\u00e9rieur d'un angle. La valeur {@code true}
     *                         fait que l'angle "45 30" sera interpr\u00e9t\u00e9 comme "45\u00b030".
     * @return L'angle lu.
     */
    @SuppressWarnings("fallthrough")
    private synchronized Angle parse(final String source,
                                     final ParsePosition pos,
                                     final boolean spaceAsSeparator)
    {
        double degrees   = Double.NaN;
        double minutes  = Double.NaN;
        double secondes = Double.NaN;
        final int length=source.length();
        ///////////////////////////////////////////////////////////////////////////////
        // BLOC A: Analyse la cha\u00eene de caract\u00e8res 'source' et affecte aux variables //
        //         'degr\u00e9s', 'minutes' et 'secondes' les valeurs appropri\u00e9es.        //
        //         Les premi\u00e8res accolades ne servent qu'\u00e0 garder locales            //
        //         les variables sans int\u00e9r\u00eat une fois la lecture termin\u00e9e.          //
        ///////////////////////////////////////////////////////////////////////////////
        {
            /*
             * Extrait le pr\u00e9fix, s'il y en avait un. Si on tombe sur un symbole des
             * degr\u00e9s, minutes ou secondes alors qu'on n'a pas encore lu de nombre,
             * on consid\u00e8rera que la lecture a \u00e9chou\u00e9e.
             */
            final int indexStart=pos.getIndex();
            int index=skipSuffix(source, pos, PREFIX_FIELD);
            if (index>=0 && index<SYMBOLS.length) {
                pos.setErrorIndex(indexStart);
                pos.setIndex(indexStart);
                return null;
            }
            /*
             * Saute les espaces blancs qui
             * pr\u00e9c\u00e8dent le champs des degr\u00e9s.
             */
            index=pos.getIndex();
            while (index<length && Character.isSpaceChar(source.charAt(index))) index++;
            pos.setIndex(index);
            /*
             * Lit les degr\u00e9s. Notez que si aucun s\u00e9parateur ne s\u00e9parait les degr\u00e9s
             * des minutes des secondes, alors cette lecture pourra inclure plusieurs
             * champs (exemple: "DDDMMmmm"). La s\u00e9paration sera faite plus tard.
             */
            Number fieldObject=numberFormat.parse(source, pos);
            if (fieldObject==null) {
                pos.setIndex(indexStart);
                if (pos.getErrorIndex()<indexStart) {
                    pos.setErrorIndex(index);
                }
                return null;
            }
            degrees=fieldObject.doubleValue();
            int indexEndField=pos.getIndex();
            boolean swapDM=true;
BigBoss:    switch (skipSuffix(source, pos, DEGREES_FIELD)) {
                /* ----------------------------------------------
                 * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9S DEGR\u00c9S
                 * ----------------------------------------------
                 * Les degr\u00e9s \u00e9taient suivit du pr\u00e9fix d'un autre angle. Le pr\u00e9fix sera donc
                 * retourn\u00e9 dans le buffer pour un \u00e9ventuel traitement par le prochain appel
                 * \u00e0 la m\u00e9thode 'parse' et on n'ira pas plus loin dans l'analyse de la cha\u00eene.
                 */
                case PREFIX_FIELD: {
                    pos.setIndex(indexEndField);
                    break BigBoss;
                }
                /* ----------------------------------------------
                 * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9S DEGR\u00c9S
                 * ----------------------------------------------
                 * On a trouv\u00e9 le symbole des secondes au lieu de celui des degr\u00e9s. On fait
                 * la correction dans les variables 'degr\u00e9s' et 'secondes' et on consid\u00e8re
                 * que la lecture est termin\u00e9e.
                 */
                case SECONDS_FIELD: {
                    secondes = degrees;
                    degrees = Double.NaN;
                    break BigBoss;
                }
                /* ----------------------------------------------
                 * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9S DEGR\u00c9S
                 * ----------------------------------------------
                 * Aucun symbole ne suit les degr\u00e9s. Des minutes sont-elles attendues?
                 * Si oui, on fera comme si le symbole des degr\u00e9s avait \u00e9t\u00e9 l\u00e0. Sinon,
                 * on consid\u00e8rera que la lecture est termin\u00e9e.
                 */
                default: {
                    if (width1==0)         break BigBoss;
                    if (!spaceAsSeparator) break BigBoss;
                    // fall through
                }
                /* ----------------------------------------------
                 * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9S DEGR\u00c9S
                 * ----------------------------------------------
                 * Un symbole des degr\u00e9s a \u00e9t\u00e9 explicitement trouv\u00e9. Les degr\u00e9s sont peut-\u00eatre
                 * suivit des minutes. On proc\u00e8dera donc \u00e0 la lecture du prochain nombre, puis
                 * \u00e0 l'analyse du symbole qui le suit.
                 */
                case DEGREES_FIELD: {
                    final int indexStartField = index = pos.getIndex();
                    while (index<length && Character.isSpaceChar(source.charAt(index))) {
                        index++;
                    }
                    if (!spaceAsSeparator && index!=indexStartField) {
                        break BigBoss;
                    }
                    pos.setIndex(index);
                    fieldObject=numberFormat.parse(source, pos);
                    if (fieldObject==null) {
                        pos.setIndex(indexStartField);
                        break BigBoss;
                    }
                    indexEndField = pos.getIndex();
                    minutes = fieldObject.doubleValue();
                    switch (skipSuffix(source, pos, (width1!=0) ? MINUTES_FIELD : PREFIX_FIELD)) {
                        /* ------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES MINUTES
                         * ------------------------------------------------
                         * Le symbole trouv\u00e9 est bel et bien celui des minutes.
                         * On continuera le bloc pour tenter de lire les secondes.
                         */
                        case MINUTES_FIELD: {
                            break; // continue outer switch
                        }
                        /* ------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES MINUTES
                         * ------------------------------------------------
                         * Un symbole des secondes a \u00e9t\u00e9 trouv\u00e9 au lieu du symbole des minutes
                         * attendu. On fera la modification dans les variables 'secondes' et
                         * 'minutes' et on consid\u00e8rera la lecture termin\u00e9e.
                         */
                        case SECONDS_FIELD: {
                            secondes = minutes;
                            minutes = Double.NaN;
                            break BigBoss;
                        }
                        /* ------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES MINUTES
                         * ------------------------------------------------
                         * Aucun symbole n'a \u00e9t\u00e9 trouv\u00e9. Les minutes \u00e9taient-elles attendues?
                         * Si oui, on les acceptera et on tentera de lire les secondes. Si non,
                         * on retourne le texte lu dans le buffer et on termine la lecture.
                         */
                        default: {
                            if (width1!=0) break; // Continue outer switch
                            // fall through
                        }
                        /* ------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES MINUTES
                         * ------------------------------------------------
                         * Au lieu des minutes, le symbole lu est celui des degr\u00e9s. On consid\u00e8re
                         * qu'il appartient au prochain angle. On retournera donc le texte lu dans
                         * le buffer et on terminera la lecture.
                         */
                        case DEGREES_FIELD: {
                            pos.setIndex(indexStartField);
                            minutes=Double.NaN;
                            break BigBoss;
                        }
                        /* ------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES MINUTES
                         * ------------------------------------------------
                         * Apr\u00e8s les minutes (qu'on accepte), on a trouv\u00e9 le pr\u00e9fix du prochain
                         * angle \u00e0 lire. On retourne ce pr\u00e9fix dans le buffer et on consid\u00e8re la
                         * lecture termin\u00e9e.
                         */
                        case PREFIX_FIELD: {
                            pos.setIndex(indexEndField);
                            break BigBoss;
                        }
                    }
                    swapDM=false;
                    // fall through
                }
                /* ----------------------------------------------
                 * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9S DEGR\u00c9S
                 * ----------------------------------------------
                 * Un symbole des minutes a \u00e9t\u00e9 trouv\u00e9 au lieu du symbole des degr\u00e9s attendu.
                 * On fera donc la modification dans les variables 'degr\u00e9s' et 'minutes'. Ces
                 * minutes sont peut-\u00eatre suivies des secondes. On tentera donc de lire le
                 * prochain nombre.
                 */
                case MINUTES_FIELD: {
                    if (swapDM) {
                        minutes = degrees;
                        degrees = Double.NaN;
                    }
                    final int indexStartField = index = pos.getIndex();
                    while (index<length && Character.isSpaceChar(source.charAt(index))) {
                        index++;
                    }
                    if (!spaceAsSeparator && index!=indexStartField) {
                        break BigBoss;
                    }
                    pos.setIndex(index);
                    fieldObject=numberFormat.parse(source, pos);
                    if (fieldObject==null) {
                        pos.setIndex(indexStartField);
                        break;
                    }
                    indexEndField = pos.getIndex();
                    secondes = fieldObject.doubleValue();
                    switch (skipSuffix(source, pos, (width2!=0) ? MINUTES_FIELD : PREFIX_FIELD)) {
                        /* -------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES SECONDES
                         * -------------------------------------------------
                         * Un symbole des secondes explicite a \u00e9t\u00e9 trouv\u00e9e.
                         * La lecture est donc termin\u00e9e.
                         */
                        case SECONDS_FIELD: {
                            break;
                        }
                        /* -------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES SECONDES
                         * -------------------------------------------------
                         * Aucun symbole n'a \u00e9t\u00e9 trouv\u00e9e. Attendait-on des secondes? Si oui, les
                         * secondes seront accept\u00e9es. Sinon, elles seront retourn\u00e9es au buffer.
                         */
                        default: {
                            if (width2!=0) break;
                            // fall through
                        }
                        /* -------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES SECONDES
                         * -------------------------------------------------
                         * Au lieu des degr\u00e9s, on a trouv\u00e9 un symbole des minutes ou des
                         * secondes. On renvoie donc le nombre et son symbole dans le buffer.
                         */
                        case MINUTES_FIELD:
                        case DEGREES_FIELD: {
                            pos.setIndex(indexStartField);
                            secondes=Double.NaN;
                            break;
                        }
                        /* -------------------------------------------------
                         * ANALYSE DU SYMBOLE SUIVANT LES PR\u00c9SUM\u00c9ES SECONDES
                         * -------------------------------------------------
                         * Apr\u00e8s les secondes (qu'on accepte), on a trouv\u00e9 le pr\u00e9fix du prochain
                         * angle \u00e0 lire. On retourne ce pr\u00e9fix dans le buffer et on consid\u00e8re la
                         * lecture termin\u00e9e.
                         */
                        case PREFIX_FIELD: {
                            pos.setIndex(indexEndField);
                            break BigBoss;
                        }
                    }
                    break;
                }
            }
        }
        ////////////////////////////////////////////////////////////////////
        // BLOC B: Prend en compte l'\u00e9ventualit\u00e9 ou le s\u00e9parateur d\u00e9cimal //
        //         aurrait \u00e9t\u00e9 absent, puis calcule l'angle en degr\u00e9s.    //
        ////////////////////////////////////////////////////////////////////
        if (minutes<0) {
            secondes = -secondes;
        }
        if (degrees<0) {
            minutes = -minutes;
            secondes = -secondes;
        }
        if (!decimalSeparator) {
            final double facteur=XMath.pow10(widthDecimal);
            if (width2!=0) {
                if (suffix1==null && Double.isNaN(secondes)) {
                    if (suffix0==null && Double.isNaN(minutes)) {
                        degrees /= facteur;
                    } else {
                        minutes /= facteur;
                    }
                } else {
                    secondes /= facteur;
                }
            } else if (Double.isNaN(secondes)) {
                if (width1!=0) {
                    if (suffix0==null && Double.isNaN(minutes)) {
                        degrees /= facteur;
                    } else {
                        minutes /= facteur;
                    }
                } else if (Double.isNaN(minutes)) {
                    degrees /= facteur;
                }
            }
        }
        /*
         * S'il n'y a rien qui permet de s\u00e9parer les degr\u00e9s des minutes (par exemple si
         * le patron est "DDDMMmmm"), alors la variable 'degr\u00e9s' englobe \u00e0 la fois les
         * degr\u00e9s, les minutes et d'\u00e9ventuelles secondes. On applique une correction ici.
         */
        if (suffix1==null && width2!=0 && Double.isNaN(secondes)) {
            double facteur = XMath.pow10(width2);
            if (suffix0==null && width1!=0 && Double.isNaN(minutes)) {
                ///////////////////
                //// DDDMMSS.s ////
                ///////////////////
                secondes = degrees;
                minutes  = (int) (degrees/facteur); // Arrondie vers 0
                secondes -= minutes*facteur;
                facteur  = XMath.pow10(width1);
                degrees   = (int) (minutes/facteur); // Arrondie vers 0
                minutes -= degrees*facteur;
            } else {
                ////////////////////
                //// DDD\u00b0MMSS.s ////
                ////////////////////
                secondes = minutes;
                minutes = (int) (minutes/facteur); // Arrondie vers 0
                secondes -= minutes*facteur;
            }
        } else if (suffix0==null && width1!=0 && Double.isNaN(minutes)) {
            /////////////////
            //// DDDMM.m ////
            /////////////////
            final double facteur = XMath.pow10(width1);
            minutes = degrees;
            degrees = (int) (degrees/facteur); // Arrondie vers 0
            minutes -= degrees*facteur;
        }
        pos.setErrorIndex(-1);
        if ( Double.isNaN(degrees))   degrees=0;
        if (!Double.isNaN(minutes))  degrees += minutes/60;
        if (!Double.isNaN(secondes)) degrees += secondes/3600;
        /////////////////////////////////////////////////////
        // BLOC C: V\u00e9rifie maintenant si l'angle ne serait //
        //         pas suivit d'un symbole N, S, E ou W.   //
        /////////////////////////////////////////////////////
        for (int index=pos.getIndex(); index<length; index++) {
            final char c=source.charAt(index);
            char cu = Character.toUpperCase(c);
            if (cu == NORTH){
                pos.setIndex(index+1); return new Latitude( degrees);
            } else if (cu == SOUTH){
                pos.setIndex(index+1); return new Latitude(-degrees);
            } else if (cu == EAST){
                pos.setIndex(index+1); return new Longitude( degrees);
            } else if (cu == WEST){
                pos.setIndex(index+1); return new Longitude(-degrees);
            }
            if (!Character.isSpaceChar(c)) {
                break;
            }
        }
        return new Angle(degrees);
    }

    /**
     * Parse a string as an angle.
     *
     * @param  source The string to parse.
     * @return The parsed string as an {@link Angle}, {@link Latitude}
     *         or {@link Longitude} object.
     * @throws ParseException if the string has not been fully parsed.
     */
    public Angle parse(final String source) throws ParseException {
        final ParsePosition pos = new ParsePosition(0);
        final Angle         ang = parse(source, pos, true);
        checkComplete(source, pos, false);
        return ang;
    }

    /**
     * Parse a substring as an angle. Default implementation invokes
     * {@link #parse(String, ParsePosition)}.
     *
     * @param source A String whose beginning should be parsed.
     * @param pos    Position where to start parsing.
     * @return       The parsed string as an {@link Angle},
     *               {@link Latitude} or {@link Longitude} object.
     */
    @Override
    public Object parseObject(final String source, final ParsePosition pos) {
        return parse(source, pos);
    }

    /**
     * Parse a string as an object. Default implementation invokes
     * {@link #parse(String)}.
     *
     * @param  source The string to parse.
     * @return The parsed string as an {@link Angle}, {@link Latitude} or
     *        {@link Longitude} object.
     * @throws ParseException if the string has not been fully parsed.
     */
    @Override
    public Object parseObject(final String source) throws ParseException {
        return parse(source);
    }

    /**
     * Interpr\u00e8te une cha\u00eene de caract\u00e8res qui devrait repr\u00e9senter un nombre.
     * Cette m\u00e9thode est utile pour lire une altitude apr\u00e8s les angles.
     *
     * @param  source Cha\u00eene de caract\u00e8res \u00e0 interpr\u00e9ter.
     * @param  pos    Position \u00e0 partir d'o\u00f9 commencer l'interpr\u00e9tation
     *                de la cha\u00eene {@code source}.
     * @return Le nombre lu comme objet {@link Number}.
     */
    final Number parseNumber(final String source, final ParsePosition pos) {
        return numberFormat.parse(source, pos);
    }

    /**
     * V\u00e9rifie si l'interpr\u00e9tation d'une cha\u00eene de caract\u00e8res a \u00e9t\u00e9 compl\u00e8te.
     * Si ce n'\u00e9tait pas le cas, lance une exception avec un message d'erreur
     * soulignant les caract\u00e8res probl\u00e9matiques.
     *
     * @param  source Cha\u00eene de caract\u00e8res qui \u00e9tait \u00e0 interpr\u00e9ter.
     * @param  pos Position \u00e0 laquelle s'est termin\u00e9e l'interpr\u00e9tation de la
     *         cha\u00eene {@code source}.
     * @param  isCoordinate {@code false} si on interpr\u00e9tait un angle,
     *         ou {@code true} si on interpr\u00e9tait une coordonn\u00e9e.
     * @throws ParseException Si la cha\u00eene {@code source} n'a pas \u00e9t\u00e9
     *         interpr\u00e9t\u00e9e dans sa totalit\u00e9.
     */
    static void checkComplete(final String source,
                              final ParsePosition pos,
                              final boolean isCoordinate)
        throws ParseException
    {
        final int length=source.length();
        final int origin=pos.getIndex();
        for (int index=origin; index<length; index++) {
            if (!Character.isWhitespace(source.charAt(index))) {
                index=pos.getErrorIndex(); if (index<0) index=origin;
                int lower=index;
                while (lower<length && Character.isWhitespace(source.charAt(lower))) {
                    lower++;
                }
                int upper=lower;
                while (upper<length && !Character.isWhitespace(source.charAt(upper))) {
                    upper++;
                }
                throw new ParseException(("Unparsable string: " + source + ", " +
                            source.substring(lower, Math.min(lower+10, upper))), index);
            }
        }
    }

    /**
     * Returns a "hash value" for this object.
     */
    @Override
    public synchronized int hashCode() {
        int c = 78236951;
        if (decimalSeparator) c^= 0xFF;
        if (prefix  !=null)   c^=         prefix.hashCode();
        if (suffix0 !=null)   c = c*37 + suffix0.hashCode();
        if (suffix1 !=null)   c^= c*37 + suffix1.hashCode();
        if (suffix2 !=null)   c^= c*37 + suffix2.hashCode();
        return c ^ (((((width0 << 8) ^ width1) << 8) ^ width2) << 8) ^ widthDecimal;
    }

    /**
     * Compare this format with the specified object for equality.
     */
    @Override
    public synchronized boolean equals(final Object obj) {
        // On ne peut pas synchroniser "obj" si on ne veut
        // pas risquer un "deadlock". Voir RFE #4210659.
        if (obj==this) {
            return true;
        }
        if (obj!=null && getClass().equals(obj.getClass())) {
            final  AngleFormat cast = (AngleFormat) obj;
            return width0           == cast.width0            &&
                   width1           == cast.width1            &&
                   width2           == cast.width2            &&
                   widthDecimal     == cast.widthDecimal      &&
                   decimalSeparator == cast.decimalSeparator  &&
                   Utilities.equals(prefix,    cast.prefix &&
                   Utilities.equals(suffix0,   cast.suffix0&&
                   Utilities.equals(suffix1,   cast.suffix1&&
                   Utilities.equals(suffix2,   cast.suffix2&&
                   Utilities.equals(numberFormat.getDecimalFormatSymbols(),
                               cast.numberFormat.getDecimalFormatSymbols());
        }
        return false;
    }

    /**
     * Returns a string representation of this object.
     */
    @Override
    public String toString() {
        return Utilities.getShortClassName(this)+'['+toPattern()+']';
    }

    private static class Utilities{
        public static boolean equals(final Object object1, final Object object2) {
            return (object1==object2) || (object1!=null && object1.equals(object2));
        }

        public static String getShortClassName(final Object object) {
            return getShortName(object!=null ? object.getClass() : null);
        }

        public static String getShortName(Class<?> classe) {
            if (classe == null) {
                return "<*>";
            }
            int dimension = 0;
            Class<?> el;
            while ((el = classe.getComponentType()) != null) {
                classe = el;
                dimension++;
            }
            String name = classe.getName();
            final int lower = name.lastIndexOf('.');
            final int upper = name.length();
            name = name.substring(lower+1, upper).replace('$','.');
            if (dimension != 0) {
                StringBuffer buffer = new StringBuffer(name);
                do {
                    buffer.append("[]");
                } while (--dimension != 0);
                name = buffer.toString();
            }
            return name;
        }
    }

    private static class XMath{
        private static final double[] POW10 = {
            1E+00, 1E+01, 1E+02, 1E+03, 1E+04, 1E+05, 1E+06, 1E+07, 1E+08, 1E+09,
            1E+10, 1E+11, 1E+12, 1E+13, 1E+14, 1E+15, 1E+16, 1E+17, 1E+18, 1E+19,
            1E+20, 1E+21, 1E+22
        };

        @SuppressWarnings("unused")
        public static double pow10(final double x) {
            final int ix = (int) x;
            if (ix == x) {
                return pow10(ix);
            }
            return Math.pow(10, x);
        }

        public static strictfp double pow10(final int x) {
            if (x >= 0) {
                if (x < POW10.length) {
                    return POW10[x];
                }
            } else if (x != Integer.MIN_VALUE) {
                final int nx = -x;
                if (nx < POW10.length) {
                    return 1 / POW10[nx];
                }
            }
            try {
                /*
                * Note: Method 'Math.pow(10,x)' has rounding errors: it doesn't
                *       always return the closest IEEE floating point
                *       representation. Method 'Double.parseDouble("1E"+x)' gives
                *       as good or better numbers for ALL integer powers, but is
                *       much slower.  The difference is usually negligible, but
                *       powers of 10 are a special case since they are often
                *       used for scaling axes or formatting human-readable output.
                *       We hope that the current workaround is only temporary.
                *       (see http://developer.java.sun.com/developer/bugParade/bugs/4358794.html).
                */
                return Double.parseDouble("1E"+x);
            } catch (NumberFormatException exception) {
                return StrictMath.pow(10, x);
            }
        }
    }
}
TOP

Related Classes of net.sourceforge.gpstools.utils.AngleFormat$Utilities

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.