package org.activityinfo.core.shared.type.converter;
import org.activityinfo.i18n.shared.I18N;
public class CoordinateParser {
public static final double MAX_LATITUDE = 90;
public static final double MAX_LONGITUDE = 180;
public enum Notation {
DDd,
DMd,
DMS
}
/**
* Provides number formatting & parsing. Extracted from the class to allow
* for testing.
*/
public static interface NumberFormatter {
/*
* Formats a coordinate as Degree-decimal, with exactly six decimal places and always with a
* sign prefix. Equivalent the formatting pattern "+0.000000;-0.000000"
*/
String formatDDd(double value);
String formatShortFraction(double value);
String formatInt(double value);
double parseDouble(String string);
}
private static final double MINUTES_PER_DEGREE = 60;
private static final double SECONDS_PER_DEGREE = 3600;
private static final double MIN_MINUTES = 0;
private static final double MAX_MINUTES = 60;
private static final double MIN_SECONDS = 0;
private static final double MAX_SECONDS = 60;
private static final String DECIMAL_SEPARATORS = ".,";
private final String posHemiChars;
private final String negHemiChars;
private double minValue;
private double maxValue;
private final String noNumberErrorMessage = I18N.CONSTANTS.noNumber();
private final String tooManyNumbersErrorMessage = I18N.CONSTANTS.tooManyNumbers();
private final String noHemisphereMessage = I18N.CONSTANTS.noHemisphere();
private final String invalidSecondsMessage = I18N.CONSTANTS.invalidMinutes();
private final String invalidMinutesMessage = I18N.CONSTANTS.invalidMinutes();
private Notation notation = Notation.DMS;
private boolean requireSign = true;
private final NumberFormatter numberFormatter;
public CoordinateParser(CoordinateAxis axis, NumberFormatter numberFormatter) {
this.numberFormatter = numberFormatter;
switch (axis) {
case LATITUDE:
posHemiChars = I18N.CONSTANTS.northHemiChars();
negHemiChars = I18N.CONSTANTS.southHemiChars();
break;
case LONGITUDE:
posHemiChars = I18N.CONSTANTS.eastHemiChars();
negHemiChars = I18N.CONSTANTS.westHemiChars();
break;
default:
throw new IllegalArgumentException("Axis: " + axis);
}
}
public void setRequireSign(boolean requireSign) {
this.requireSign = requireSign;
}
public Double parse(String value) throws CoordinateFormatException {
if (value == null) {
return null;
}
StringBuffer[] numbers = new StringBuffer[]{
new StringBuffer(),
new StringBuffer(),
new StringBuffer()};
int numberIndex = 0;
int i;
/*
* To assure correctness, we're going to insist that the user explicitly
* enter the hemisphere (+/-/N/S).
*
* However, if the bounds dictate that the coordinate is in one
* hemisphere, then we can assume the sign.
*/
double sign = maybeInferSignFromBounds();
for (i = 0; i != value.length(); ++i) {
char c = value.charAt(i);
if (isNegHemiChar(c)) {
sign = -1;
} else if (isPosHemiChar(c)) {
sign = 1;
} else if (isNumberPart(c)) {
if (numberIndex > 2) {
throw new CoordinateFormatException(
tooManyNumbersErrorMessage);
}
numbers[numberIndex].append(c);
} else if (numberIndex != 2 && numbers[numberIndex].length() > 0) {
// advance to the next token on anything else-- whitespace,
// symbols like ' " ° -- we won't insist that they are used
// in the right place
numberIndex++;
}
}
if (sign == 0) {
if (requireSign) {
throw new CoordinateFormatException(noHemisphereMessage);
} else {
sign = 1;
}
}
return parseCoordinate(numbers) * sign;
}
private double maybeInferSignFromBounds() {
double sign = 0;
if (maxValue < 0) {
sign = -1;
} else if (minValue > 0) {
sign = +1;
}
return sign;
}
private boolean isNumberPart(char c) {
return Character.isDigit(c) || DECIMAL_SEPARATORS.indexOf(c) != -1;
}
private boolean isPosHemiChar(char c) {
return c == '+' || posHemiChars.indexOf(c) != -1;
}
private boolean isNegHemiChar(char c) {
return c == '-' || negHemiChars.indexOf(c) != -1;
}
private double parseCoordinate(StringBuffer[] tokens)
throws CoordinateFormatException {
if (tokens[0].length() == 0) {
throw new CoordinateFormatException(noNumberErrorMessage);
}
double coordinate = Double.parseDouble(tokens[0].toString());
notation = Notation.DDd;
if (tokens[1].length() != 0) {
double minutes = numberFormatter.parseDouble(tokens[1].toString());
if (minutes < MIN_MINUTES || minutes > MAX_MINUTES) {
throw new CoordinateFormatException(invalidMinutesMessage);
}
coordinate += minutes / MINUTES_PER_DEGREE;
notation = Notation.DMd;
}
if (tokens[2].length() != 0) {
double seconds = numberFormatter.parseDouble(tokens[2].toString());
if (seconds < MIN_SECONDS || seconds > MAX_SECONDS) {
throw new CoordinateFormatException(invalidSecondsMessage);
}
notation = Notation.DMS;
coordinate += seconds / SECONDS_PER_DEGREE;
}
return coordinate;
}
/**
* Formats coordinate value into Degree-Minute-decimal notation
*/
public String formatAsDMd(double value) {
double degrees = Math.floor(Math.abs(value));
double minutes = (Math.abs(value) - degrees);
StringBuilder sb = new StringBuilder();
sb.append(numberFormatter.formatInt(Math.abs(degrees))).append("° ");
sb.append(numberFormatter.formatShortFraction(minutes)).append("' ");
sb.append(hemisphereChar(value));
return sb.toString();
}
public String formatAsDDd(double coordinate) {
return numberFormatter.formatDDd(coordinate);
}
/**
* Formats coordinate value into Degree-Minute-Second notation
*/
public String formatAsDMS(double value) {
double absv = Math.abs(value);
double degrees = Math.floor(absv);
double minutes = ((absv - degrees) * 60.0);
double seconds = ((minutes - Math.floor(minutes)) * 60.0);
minutes = Math.floor(minutes);
StringBuilder sb = new StringBuilder();
sb.append(numberFormatter.formatInt(Math.abs(degrees))).append("° ");
sb.append(numberFormatter.formatInt(minutes)).append("' ");
sb.append(numberFormatter.formatShortFraction(seconds)).append("\" ");
sb.append(hemisphereChar(value));
return sb.toString();
}
public String format(double coordinate) {
return format(notation, coordinate);
}
public String format(Notation notation, double coordinate) {
switch (notation) {
case DDd:
return formatAsDDd(coordinate);
case DMd:
return formatAsDMd(coordinate);
default:
case DMS:
return formatAsDMS(coordinate);
}
}
private char hemisphereChar(double value) {
if (Math.signum(value) < 0) {
return negHemiChars.charAt(0);
} else {
return posHemiChars.charAt(0);
}
}
public Notation getNotation() {
return notation;
}
public void setNotation(Notation notation) {
this.notation = notation;
}
public double getMinValue() {
return minValue;
}
public void setMinValue(double minValue) {
this.minValue = minValue;
}
public double getMaxValue() {
return maxValue;
}
public void setMaxValue(double maxValue) {
this.maxValue = maxValue;
}
}