1 package tim.prune.data;
3 import java.text.DecimalFormat;
4 import java.text.NumberFormat;
5 import java.util.Locale;
8 * Class to represent a lat/long coordinate
9 * and provide conversion functions
11 public abstract class Coordinate
13 public static final int NO_CARDINAL = -1;
14 public static final int NORTH = 0;
15 public static final int EAST = 1;
16 public static final int SOUTH = 2;
17 public static final int WEST = 3;
18 private static final char[] PRINTABLE_CARDINALS = {'N', 'E', 'S', 'W'};
19 public static final int FORMAT_DEG_MIN_SEC = 10;
20 public static final int FORMAT_DEG_MIN = 11;
21 public static final int FORMAT_DEG = 12;
22 public static final int FORMAT_DEG_WITHOUT_CARDINAL = 13;
23 public static final int FORMAT_DEG_WHOLE_MIN = 14;
24 public static final int FORMAT_DEG_MIN_SEC_WITH_SPACES = 15;
25 public static final int FORMAT_CARDINAL = 16;
26 public static final int FORMAT_DECIMAL_FORCE_POINT = 17;
27 public static final int FORMAT_NONE = 19;
29 /** Number formatter for fixed decimals with forced decimal point */
30 private static final NumberFormat EIGHT_DP = NumberFormat.getNumberInstance(Locale.UK);
31 // Select the UK locale for this formatter so that decimal point is always used (not comma)
33 if (EIGHT_DP instanceof DecimalFormat) ((DecimalFormat) EIGHT_DP).applyPattern("0.00000000");
35 /** Number formatter for fixed decimals with forced decimal point */
36 private static final NumberFormat FIVE_DP = NumberFormat.getNumberInstance(Locale.UK);
38 if (FIVE_DP instanceof DecimalFormat) ((DecimalFormat) FIVE_DP).applyPattern("0.00000");
42 private boolean _valid = false;
43 private boolean _cardinalGuessed = false;
44 protected int _cardinal = NORTH;
45 private int _degrees = 0;
46 private int _minutes = 0;
47 private int _seconds = 0;
48 private int _fracs = 0;
49 private int _fracDenom = 0;
50 private String _originalString = null;
51 private int _originalFormat = FORMAT_NONE;
52 private double _asDouble = 0.0;
56 * Constructor given String
57 * @param inString string to parse
59 public Coordinate(String inString)
61 _originalString = inString;
65 inString = inString.trim();
66 strLen = inString.length();
70 // Check for cardinal character either at beginning or end
71 boolean hasCardinal = true;
72 _cardinal = getCardinal(inString.charAt(0), inString.charAt(strLen-1));
73 if (_cardinal == NO_CARDINAL) {
75 // use default from concrete subclass
76 _cardinal = getDefaultCardinal();
77 _cardinalGuessed = true;
79 else if (isJustNumber(inString)) {
82 _cardinalGuessed = true;
85 // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
87 boolean isNumeric = false;
89 long[] fields = new long[4]; // needs to be long for lengthy decimals
90 long[] denoms = new long[4];
91 boolean[] otherDelims = new boolean[5]; // remember whether delimiters have non-decimal chars
94 // Loop over characters in input string, populating fields array
95 for (int i=0; i<strLen; i++)
97 currChar = inString.charAt(i);
98 if (currChar >= '0' && currChar <= '9')
104 denoms[numFields-1] = 1;
106 if (denoms[numFields-1] < 1E18) // ignore trailing characters if too big for long
108 fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0');
109 denoms[numFields-1] *= 10;
115 // Remember delimiters
116 if (currChar != ',' && currChar != '.') {otherDelims[numFields] = true;}
119 _valid = (numFields > 0);
121 catch (ArrayIndexOutOfBoundsException obe)
123 // more than four fields found - unable to parse
126 // parse fields according to number found
127 _degrees = (int) fields[0];
128 _asDouble = _degrees;
129 _originalFormat = hasCardinal ? FORMAT_DEG : FORMAT_DEG_WITHOUT_CARDINAL;
135 // String is just decimal degrees
136 double numMins = fields[1] * 60.0 / denoms[1];
137 _minutes = (int) numMins;
138 double numSecs = (numMins - _minutes) * 60.0;
139 _seconds = (int) numSecs;
140 _fracs = (int) ((numSecs - _seconds) * 10);
141 _asDouble = _degrees + 1.0 * fields[1] / denoms[1];
145 // String is degrees and minutes (due to non-decimal separator)
146 _originalFormat = FORMAT_DEG_MIN;
147 _minutes = (int) fields[1];
150 _asDouble = 1.0 * _degrees + (_minutes / 60.0);
153 // Check for exponential degrees like 1.3E-6
154 else if (numFields == 3 && !otherDelims[1] && otherDelims[2] && isJustNumber(inString))
156 _originalFormat = FORMAT_DEG;
157 _asDouble = Math.abs(Double.parseDouble(inString)); // must succeed if isJustNumber has given true
158 // now we can ignore the fields and just use this double
159 _degrees = (int) _asDouble;
160 double numMins = (_asDouble - _degrees) * 60.0;
161 _minutes = (int) numMins;
162 double numSecs = (numMins - _minutes) * 60.0;
163 _seconds = (int) numSecs;
164 _fracs = (int) ((numSecs - _seconds) * 10);
166 // Differentiate between d-m.f and d-m-s using . or ,
167 else if (numFields == 3 && !otherDelims[2])
169 // String is degrees-minutes.fractions
170 _originalFormat = FORMAT_DEG_MIN;
171 _minutes = (int) fields[1];
172 double numSecs = fields[2] * 60.0 / denoms[2];
173 _seconds = (int) numSecs;
174 _fracs = (int) ((numSecs - _seconds) * 10);
175 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (numSecs / 3600.0);
177 else if (numFields == 4 || numFields == 3)
179 // String is degrees-minutes-seconds.fractions
180 _originalFormat = FORMAT_DEG_MIN_SEC;
181 _minutes = (int) fields[1];
182 _seconds = (int) fields[2];
183 _fracs = (int) fields[3];
184 _fracDenom = (int) denoms[3];
185 if (_fracDenom < 1) {_fracDenom = 1;}
186 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
188 if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
189 _asDouble = -_asDouble;
191 _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom)
192 && Math.abs(_asDouble) <= getMaxDegrees();
199 * Get the cardinal from the given character
200 * @param inFirstChar first character from string
201 * @param inLastChar last character from string
203 private int getCardinal(char inFirstChar, char inLastChar)
205 // Try leading character first
206 int cardinal = getCardinal(inFirstChar);
207 // if not there, try trailing character
208 if (cardinal == NO_CARDINAL) {
209 cardinal = getCardinal(inLastChar);
215 * @return true if cardinal was guessed, false if parsed
217 public boolean getCardinalGuessed() {
218 return _cardinalGuessed;
222 * Get the cardinal from the given character
223 * @param inChar character from file
225 protected abstract int getCardinal(char inChar);
228 * @return the default cardinal for the subclass
230 protected abstract int getDefaultCardinal();
233 * @return the maximum degree range for this coordinate
235 protected abstract int getMaxDegrees();
240 * @param inValue value of coordinate
241 * @param inFormat format to use
242 * @param inCardinal cardinal
244 protected Coordinate(double inValue, int inFormat, int inCardinal)
247 // Calculate degrees, minutes, seconds
248 _degrees = (int) Math.abs(inValue);
249 double numMins = (Math.abs(_asDouble)-_degrees) * 60.0;
250 _minutes = (int) numMins;
251 double numSecs = (numMins - _minutes) * 60.0;
252 _seconds = (int) numSecs;
253 _fracs = (int) ((numSecs - _seconds) * 10);
254 _fracDenom = 10; // fixed for now
255 // Make a string to display on screen
256 _cardinal = inCardinal;
257 _originalFormat = FORMAT_NONE;
258 if (inFormat == FORMAT_NONE) inFormat = FORMAT_DEG_WITHOUT_CARDINAL;
259 _originalString = output(inFormat);
260 _originalFormat = inFormat;
266 * @return coordinate as a double
268 public double getDouble()
274 * @return true if Coordinate is valid
276 public boolean isValid()
282 * Compares two Coordinates for equality
283 * @param inOther other Coordinate object with which to compare
284 * @return true if the two objects are equal
286 public boolean equals(Coordinate inOther)
288 return (_asDouble == inOther._asDouble);
293 * Output the Coordinate in the given format
294 * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
295 * @return String for output
297 public String output(int inFormat)
299 String answer = _originalString;
300 if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
302 // TODO: allow specification of precision for output of d-m and d
303 // format as specified
306 case FORMAT_DEG_MIN_SEC:
308 StringBuffer buffer = new StringBuffer();
309 buffer.append(PRINTABLE_CARDINALS[_cardinal])
310 .append(threeDigitString(_degrees)).append('\u00B0')
311 .append(twoDigitString(_minutes)).append('\'')
312 .append(twoDigitString(_seconds)).append('.')
313 .append(formatFraction(_fracs, _fracDenom));
314 answer = buffer.toString();
319 answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "\u00B0"
320 + FIVE_DP.format((Math.abs(_asDouble) - _degrees) * 60.0) + "'";
323 case FORMAT_DEG_WHOLE_MIN:
326 int min = (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5);
330 answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(deg) + "\u00B0" + min + "'";
334 case FORMAT_DEG_WITHOUT_CARDINAL:
336 if (_originalFormat != FORMAT_DEG_WITHOUT_CARDINAL)
338 answer = (_asDouble<0.0?"-":"")
339 + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
343 case FORMAT_DECIMAL_FORCE_POINT:
345 // Forcing a decimal point instead of system-dependent commas etc
346 if (_originalFormat != FORMAT_DEG_WITHOUT_CARDINAL || answer.indexOf('.') < 0) {
347 answer = EIGHT_DP.format(_asDouble);
351 case FORMAT_DEG_MIN_SEC_WITH_SPACES:
353 // Note: cardinal not needed as this format is only for exif, which has cardinal separately
354 answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
357 case FORMAT_CARDINAL:
359 answer = "" + PRINTABLE_CARDINALS[_cardinal];
368 * Format the fraction part of seconds value
369 * @param inFrac fractional part eg 123
370 * @param inDenom denominator of fraction eg 10000
371 * @return String describing fraction, in this case 0123
373 private static final String formatFraction(int inFrac, int inDenom)
375 if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
376 String denomString = "" + inDenom;
377 int reqdLen = denomString.length() - 1;
378 String result = denomString + inFrac;
379 int resultLen = result.length();
380 return result.substring(resultLen - reqdLen);
385 * Format an integer to a two-digit String
386 * @param inNumber number to format
387 * @return two-character String
389 private static String twoDigitString(int inNumber)
391 if (inNumber <= 0) return "00";
392 if (inNumber < 10) return "0" + inNumber;
393 if (inNumber < 100) return "" + inNumber;
394 return "" + (inNumber % 100);
399 * Format an integer to a three-digit String for degrees
400 * @param inNumber number to format
401 * @return three-character String
403 private static String threeDigitString(int inNumber)
405 if (inNumber <= 0) return "000";
406 if (inNumber < 10) return "00" + inNumber;
407 if (inNumber < 100) return "0" + inNumber;
408 return "" + (inNumber % 1000);
413 * Create a new Coordinate between two others
414 * @param inStart start coordinate
415 * @param inEnd end coordinate
416 * @param inIndex index of point
417 * @param inNumPoints number of points to interpolate
418 * @return new Coordinate object
420 public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
421 int inIndex, int inNumPoints)
423 return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
428 * Create a new Coordinate between two others
429 * @param inStart start coordinate
430 * @param inEnd end coordinate
431 * @param inFraction fraction from start to end
432 * @return new Coordinate object
434 public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
437 double startValue = inStart.getDouble();
438 double endValue = inEnd.getDouble();
439 double newValue = startValue + (endValue - startValue) * inFraction;
440 Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat);
446 * Make a new Coordinate according to subclass
447 * @param inValue double value
448 * @param inFormat format to use
449 * @return object of Coordinate subclass
451 protected abstract Coordinate makeNew(double inValue, int inFormat);
454 * Try to parse the given string
455 * @param inString string to check
456 * @return true if it can be parsed as a number
458 private static boolean isJustNumber(String inString)
460 boolean justNum = false;
462 double x = Double.parseDouble(inString);
463 justNum = (x >= -180.0 && x <= 360.0);
465 catch (NumberFormatException nfe) {} // flag remains false
470 * Create a String representation for debug
471 * @return String describing coordinate value
473 public String toString()
475 return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
476 + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;