--- /dev/null
+package tim.prune.data;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * Class to represent a lat/long coordinate
+ * and provide conversion functions
+ */
+public abstract class Coordinate
+{
+ public static final int NO_CARDINAL = -1;
+ public static final int NORTH = 0;
+ public static final int EAST = 1;
+ public static final int SOUTH = 2;
+ public static final int WEST = 3;
+ private static final char[] PRINTABLE_CARDINALS = {'N', 'E', 'S', 'W'};
+ public static final int FORMAT_DEG_MIN_SEC = 10;
+ public static final int FORMAT_DEG_MIN = 11;
+ public static final int FORMAT_DEG = 12;
+ public static final int FORMAT_DEG_WITHOUT_CARDINAL = 13;
+ public static final int FORMAT_DEG_WHOLE_MIN = 14;
+ public static final int FORMAT_DEG_MIN_SEC_WITH_SPACES = 15;
+ public static final int FORMAT_CARDINAL = 16;
+ public static final int FORMAT_DECIMAL_FORCE_POINT = 17;
+ public static final int FORMAT_NONE = 19;
+
+ /** Number formatter for fixed decimals with forced decimal point */
+ private static final NumberFormat EIGHT_DP = NumberFormat.getNumberInstance(Locale.UK);
+ // Select the UK locale for this formatter so that decimal point is always used (not comma)
+ static {
+ if (EIGHT_DP instanceof DecimalFormat) ((DecimalFormat) EIGHT_DP).applyPattern("0.00000000");
+ }
+ /** Number formatter for fixed decimals with forced decimal point */
+ private static final NumberFormat FIVE_DP = NumberFormat.getNumberInstance(Locale.UK);
+ static {
+ if (FIVE_DP instanceof DecimalFormat) ((DecimalFormat) FIVE_DP).applyPattern("0.00000");
+ }
+
+ // Instance variables
+ private boolean _valid = false;
+ private boolean _cardinalGuessed = false;
+ protected int _cardinal = NORTH;
+ private int _degrees = 0;
+ private int _minutes = 0;
+ private int _seconds = 0;
+ private int _fracs = 0;
+ private int _fracDenom = 0;
+ private String _originalString = null;
+ private int _originalFormat = FORMAT_NONE;
+ private double _asDouble = 0.0;
+
+
+ /**
+ * Constructor given String
+ * @param inString string to parse
+ */
+ public Coordinate(String inString)
+ {
+ _originalString = inString;
+ int strLen = 0;
+ if (inString != null)
+ {
+ inString = inString.trim();
+ strLen = inString.length();
+ }
+ if (strLen > 0)
+ {
+ // Check for cardinal character either at beginning or end
+ boolean hasCardinal = true;
+ _cardinal = getCardinal(inString.charAt(0), inString.charAt(strLen-1));
+ if (_cardinal == NO_CARDINAL) {
+ hasCardinal = false;
+ // use default from concrete subclass
+ _cardinal = getDefaultCardinal();
+ _cardinalGuessed = true;
+ }
+ else if (isJustNumber(inString)) {
+ // it's just a number
+ hasCardinal = false;
+ _cardinalGuessed = true;
+ }
+
+ // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
+ int numFields = 0;
+ boolean isNumeric = false;
+ char currChar;
+ long[] fields = new long[4]; // needs to be long for lengthy decimals
+ long[] denoms = new long[4];
+ boolean[] otherDelims = new boolean[5]; // remember whether delimiters have non-decimal chars
+ try
+ {
+ // Loop over characters in input string, populating fields array
+ for (int i=0; i<strLen; i++)
+ {
+ currChar = inString.charAt(i);
+ if (currChar >= '0' && currChar <= '9')
+ {
+ if (!isNumeric)
+ {
+ isNumeric = true;
+ numFields++;
+ denoms[numFields-1] = 1;
+ }
+ if (denoms[numFields-1] < 1E18) // ignore trailing characters if too big for long
+ {
+ fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0');
+ denoms[numFields-1] *= 10;
+ }
+ }
+ else
+ {
+ isNumeric = false;
+ // Remember delimiters
+ if (currChar != ',' && currChar != '.') {otherDelims[numFields] = true;}
+ }
+ }
+ _valid = (numFields > 0);
+ }
+ catch (ArrayIndexOutOfBoundsException obe)
+ {
+ // more than four fields found - unable to parse
+ _valid = false;
+ }
+ // parse fields according to number found
+ _degrees = (int) fields[0];
+ _asDouble = _degrees;
+ _originalFormat = hasCardinal ? FORMAT_DEG : FORMAT_DEG_WITHOUT_CARDINAL;
+ _fracDenom = 10;
+ if (numFields == 2)
+ {
+ if (!otherDelims[1])
+ {
+ // String is just decimal degrees
+ double numMins = fields[1] * 60.0 / denoms[1];
+ _minutes = (int) numMins;
+ double numSecs = (numMins - _minutes) * 60.0;
+ _seconds = (int) numSecs;
+ _fracs = (int) ((numSecs - _seconds) * 10);
+ _asDouble = _degrees + 1.0 * fields[1] / denoms[1];
+ }
+ else
+ {
+ // String is degrees and minutes (due to non-decimal separator)
+ _originalFormat = FORMAT_DEG_MIN;
+ _minutes = (int) fields[1];
+ _seconds = 0;
+ _fracs = 0;
+ _asDouble = 1.0 * _degrees + (_minutes / 60.0);
+ }
+ }
+ // Check for exponential degrees like 1.3E-6
+ else if (numFields == 3 && !otherDelims[1] && otherDelims[2] && isJustNumber(inString))
+ {
+ _originalFormat = FORMAT_DEG;
+ _asDouble = Math.abs(Double.parseDouble(inString)); // must succeed if isJustNumber has given true
+ // now we can ignore the fields and just use this double
+ _degrees = (int) _asDouble;
+ double numMins = (_asDouble - _degrees) * 60.0;
+ _minutes = (int) numMins;
+ double numSecs = (numMins - _minutes) * 60.0;
+ _seconds = (int) numSecs;
+ _fracs = (int) ((numSecs - _seconds) * 10);
+ }
+ // Differentiate between d-m.f and d-m-s using . or ,
+ else if (numFields == 3 && !otherDelims[2])
+ {
+ // String is degrees-minutes.fractions
+ _originalFormat = FORMAT_DEG_MIN;
+ _minutes = (int) fields[1];
+ double numSecs = fields[2] * 60.0 / denoms[2];
+ _seconds = (int) numSecs;
+ _fracs = (int) ((numSecs - _seconds) * 10);
+ _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (numSecs / 3600.0);
+ }
+ else if (numFields == 4 || numFields == 3)
+ {
+ // String is degrees-minutes-seconds.fractions
+ _originalFormat = FORMAT_DEG_MIN_SEC;
+ _minutes = (int) fields[1];
+ _seconds = (int) fields[2];
+ _fracs = (int) fields[3];
+ _fracDenom = (int) denoms[3];
+ if (_fracDenom < 1) {_fracDenom = 1;}
+ _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
+ }
+ if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
+ _asDouble = -_asDouble;
+ // validate fields
+ _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom)
+ && Math.abs(_asDouble) <= getMaxDegrees();
+ }
+ else _valid = false;
+ }
+
+
+ /**
+ * Get the cardinal from the given character
+ * @param inFirstChar first character from string
+ * @param inLastChar last character from string
+ */
+ private int getCardinal(char inFirstChar, char inLastChar)
+ {
+ // Try leading character first
+ int cardinal = getCardinal(inFirstChar);
+ // if not there, try trailing character
+ if (cardinal == NO_CARDINAL) {
+ cardinal = getCardinal(inLastChar);
+ }
+ return cardinal;
+ }
+
+ /**
+ * @return true if cardinal was guessed, false if parsed
+ */
+ public boolean getCardinalGuessed() {
+ return _cardinalGuessed;
+ }
+
+ /**
+ * Get the cardinal from the given character
+ * @param inChar character from file
+ */
+ protected abstract int getCardinal(char inChar);
+
+ /**
+ * @return the default cardinal for the subclass
+ */
+ protected abstract int getDefaultCardinal();
+
+ /**
+ * @return the maximum degree range for this coordinate
+ */
+ protected abstract int getMaxDegrees();
+
+
+ /**
+ * Constructor
+ * @param inValue value of coordinate
+ * @param inFormat format to use
+ * @param inCardinal cardinal
+ */
+ protected Coordinate(double inValue, int inFormat, int inCardinal)
+ {
+ _asDouble = inValue;
+ // Calculate degrees, minutes, seconds
+ _degrees = (int) Math.abs(inValue);
+ double numMins = (Math.abs(_asDouble)-_degrees) * 60.0;
+ _minutes = (int) numMins;
+ double numSecs = (numMins - _minutes) * 60.0;
+ _seconds = (int) numSecs;
+ _fracs = (int) ((numSecs - _seconds) * 10);
+ _fracDenom = 10; // fixed for now
+ // Make a string to display on screen
+ _cardinal = inCardinal;
+ _originalFormat = FORMAT_NONE;
+ if (inFormat == FORMAT_NONE) inFormat = FORMAT_DEG_WITHOUT_CARDINAL;
+ _originalString = output(inFormat);
+ _originalFormat = inFormat;
+ _valid = true;
+ }
+
+
+ /**
+ * @return coordinate as a double
+ */
+ public double getDouble()
+ {
+ return _asDouble;
+ }
+
+ /**
+ * @return true if Coordinate is valid
+ */
+ public boolean isValid()
+ {
+ return _valid;
+ }
+
+ /**
+ * Compares two Coordinates for equality
+ * @param inOther other Coordinate object with which to compare
+ * @return true if the two objects are equal
+ */
+ public boolean equals(Coordinate inOther)
+ {
+ return (_asDouble == inOther._asDouble);
+ }
+
+
+ /**
+ * Output the Coordinate in the given format
+ * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
+ * @return String for output
+ */
+ public String output(int inFormat)
+ {
+ String answer = _originalString;
+ if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
+ {
+ // TODO: allow specification of precision for output of d-m and d
+ // format as specified
+ switch (inFormat)
+ {
+ case FORMAT_DEG_MIN_SEC:
+ {
+ StringBuffer buffer = new StringBuffer();
+ buffer.append(PRINTABLE_CARDINALS[_cardinal])
+ .append(threeDigitString(_degrees)).append('\u00B0')
+ .append(twoDigitString(_minutes)).append('\'')
+ .append(twoDigitString(_seconds)).append('.')
+ .append(formatFraction(_fracs, _fracDenom));
+ answer = buffer.toString();
+ break;
+ }
+ case FORMAT_DEG_MIN:
+ {
+ answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "\u00B0"
+ + FIVE_DP.format((Math.abs(_asDouble) - _degrees) * 60.0) + "'";
+ break;
+ }
+ case FORMAT_DEG_WHOLE_MIN:
+ {
+ int deg = _degrees;
+ int min = (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5);
+ if (min == 60) {
+ min = 0; deg++;
+ }
+ answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(deg) + "\u00B0" + min + "'";
+ break;
+ }
+ case FORMAT_DEG:
+ case FORMAT_DEG_WITHOUT_CARDINAL:
+ {
+ if (_originalFormat != FORMAT_DEG_WITHOUT_CARDINAL)
+ {
+ answer = (_asDouble<0.0?"-":"")
+ + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
+ }
+ break;
+ }
+ case FORMAT_DECIMAL_FORCE_POINT:
+ {
+ // Forcing a decimal point instead of system-dependent commas etc
+ if (_originalFormat != FORMAT_DEG_WITHOUT_CARDINAL || answer.indexOf('.') < 0) {
+ answer = EIGHT_DP.format(_asDouble);
+ }
+ break;
+ }
+ case FORMAT_DEG_MIN_SEC_WITH_SPACES:
+ {
+ // Note: cardinal not needed as this format is only for exif, which has cardinal separately
+ answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
+ break;
+ }
+ case FORMAT_CARDINAL:
+ {
+ answer = "" + PRINTABLE_CARDINALS[_cardinal];
+ break;
+ }
+ }
+ }
+ return answer;
+ }
+
+ /**
+ * Format the fraction part of seconds value
+ * @param inFrac fractional part eg 123
+ * @param inDenom denominator of fraction eg 10000
+ * @return String describing fraction, in this case 0123
+ */
+ private static final String formatFraction(int inFrac, int inDenom)
+ {
+ if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
+ String denomString = "" + inDenom;
+ int reqdLen = denomString.length() - 1;
+ String result = denomString + inFrac;
+ int resultLen = result.length();
+ return result.substring(resultLen - reqdLen);
+ }
+
+
+ /**
+ * Format an integer to a two-digit String
+ * @param inNumber number to format
+ * @return two-character String
+ */
+ private static String twoDigitString(int inNumber)
+ {
+ if (inNumber <= 0) return "00";
+ if (inNumber < 10) return "0" + inNumber;
+ if (inNumber < 100) return "" + inNumber;
+ return "" + (inNumber % 100);
+ }
+
+
+ /**
+ * Format an integer to a three-digit String for degrees
+ * @param inNumber number to format
+ * @return three-character String
+ */
+ private static String threeDigitString(int inNumber)
+ {
+ if (inNumber <= 0) return "000";
+ if (inNumber < 10) return "00" + inNumber;
+ if (inNumber < 100) return "0" + inNumber;
+ return "" + (inNumber % 1000);
+ }
+
+
+ /**
+ * Create a new Coordinate between two others
+ * @param inStart start coordinate
+ * @param inEnd end coordinate
+ * @param inIndex index of point
+ * @param inNumPoints number of points to interpolate
+ * @return new Coordinate object
+ */
+ public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
+ int inIndex, int inNumPoints)
+ {
+ return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
+ }
+
+
+ /**
+ * Create a new Coordinate between two others
+ * @param inStart start coordinate
+ * @param inEnd end coordinate
+ * @param inFraction fraction from start to end
+ * @return new Coordinate object
+ */
+ public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
+ double inFraction)
+ {
+ double startValue = inStart.getDouble();
+ double endValue = inEnd.getDouble();
+ double newValue = startValue + (endValue - startValue) * inFraction;
+ Coordinate answer = inStart.makeNew(newValue, Coordinate.FORMAT_DECIMAL_FORCE_POINT);
+ return answer;
+ }
+
+
+ /**
+ * Make a new Coordinate according to subclass
+ * @param inValue double value
+ * @param inFormat format to use
+ * @return object of Coordinate subclass
+ */
+ protected abstract Coordinate makeNew(double inValue, int inFormat);
+
+ /**
+ * Try to parse the given string
+ * @param inString string to check
+ * @return true if it can be parsed as a number
+ */
+ private static boolean isJustNumber(String inString)
+ {
+ boolean justNum = false;
+ try {
+ double x = Double.parseDouble(inString);
+ justNum = (x >= -180.0 && x <= 360.0);
+ }
+ catch (NumberFormatException nfe) {} // flag remains false
+ return justNum;
+ }
+
+ /**
+ * Create a String representation for debug
+ * @return String describing coordinate value
+ */
+ public String toString()
+ {
+ return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
+ + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;
+ }
+}