/*
* 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> The integer part of degrees</td></tr>
* <tr><td>{@code d}</td><td> The fractional part of degrees</td></tr>
* <tr><td>{@code M}</td><td> The integer part of minutes</td></tr>
* <tr><td>{@code m}</td><td> The fractional part of minutes</td></tr>
* <tr><td>{@code S}</td><td> The integer part of seconds</td></tr>
* <tr><td>{@code s}</td><td> The fractional part of seconds</td></tr>
* <tr><td>{@code .}</td><td> 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);
}
}
}
}