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");
37 private boolean _valid = false;
38 protected int _cardinal = NORTH;
39 private int _degrees = 0;
40 private int _minutes = 0;
41 private int _seconds = 0;
42 private int _fracs = 0;
43 private int _fracDenom = 0;
44 private String _originalString = null;
45 private int _originalFormat = FORMAT_NONE;
46 private double _asDouble = 0.0;
50 * Constructor given String
51 * @param inString string to parse
53 public Coordinate(String inString)
55 _originalString = inString;
59 inString = inString.trim();
60 strLen = inString.length();
64 // Check for cardinal character either at beginning or end
65 boolean hasCardinal = true;
66 _cardinal = getCardinal(inString.charAt(0), inString.charAt(strLen-1));
67 if (_cardinal == NO_CARDINAL) {
69 // use default from concrete subclass
70 _cardinal = getDefaultCardinal();
73 // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
75 boolean inNumeric = false;
77 long[] fields = new long[4]; // needs to be long for lengthy decimals
78 long[] denoms = new long[4];
79 boolean[] otherDelims = new boolean[5]; // remember whether delimiters have non-decimal chars
82 // Loop over characters in input string, populating fields array
83 for (int i=0; i<strLen; i++)
85 currChar = inString.charAt(i);
86 if (currChar >= '0' && currChar <= '9')
92 denoms[numFields-1] = 1;
94 fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0');
95 denoms[numFields-1] *= 10;
100 // Remember delimiters
101 if (currChar != ',' && currChar != '.') {otherDelims[numFields] = true;}
104 _valid = (numFields > 0);
106 catch (ArrayIndexOutOfBoundsException obe)
108 // more than four fields found - unable to parse
111 // parse fields according to number found
112 _degrees = (int) fields[0];
113 _originalFormat = hasCardinal?FORMAT_DEG:FORMAT_DEG_WITHOUT_CARDINAL;
119 // String is just decimal degrees
120 double numMins = fields[1] * 60.0 / denoms[1];
121 _minutes = (int) numMins;
122 double numSecs = (numMins - _minutes) * 60.0;
123 _seconds = (int) numSecs;
124 _fracs = (int) ((numSecs - _seconds) * 10);
125 _asDouble = _degrees + 1.0 * fields[1] / denoms[1];
129 // String is degrees and minutes (due to non-decimal separator)
130 _originalFormat = FORMAT_DEG_MIN;
131 _minutes = (int) fields[1];
134 _asDouble = 1.0 * _degrees + (_minutes / 60.0);
137 // Differentiate between d-m.f and d-m-s using . or ,
138 else if (numFields == 3 && !otherDelims[2])
140 // String is degrees-minutes.fractions
141 _originalFormat = FORMAT_DEG_MIN;
142 _minutes = (int) fields[1];
143 double numSecs = fields[2] * 60.0 / denoms[2];
144 _seconds = (int) numSecs;
145 _fracs = (int) ((numSecs - _seconds) * 10);
146 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (numSecs / 3600.0);
148 else if (numFields == 4 || numFields == 3)
150 // String is degrees-minutes-seconds.fractions
151 _originalFormat = FORMAT_DEG_MIN_SEC;
152 _minutes = (int) fields[1];
153 _seconds = (int) fields[2];
154 _fracs = (int) fields[3];
155 _fracDenom = (int) denoms[3];
156 if (_fracDenom < 1) {_fracDenom = 1;}
157 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
159 if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
160 _asDouble = -_asDouble;
162 _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
169 * Get the cardinal from the given character
170 * @param inFirstChar first character from file
171 * @param inLastChar last character from file
173 protected int getCardinal(char inFirstChar, char inLastChar)
175 // Try leading character first
176 int cardinal = getCardinal(inFirstChar);
177 // if not there, try trailing character
178 if (cardinal == NO_CARDINAL) {
179 cardinal = getCardinal(inLastChar);
186 * Get the cardinal from the given character
187 * @param inChar character from file
189 protected abstract int getCardinal(char inChar);
192 * @return the default cardinal for the subclass
194 protected abstract int getDefaultCardinal();
197 * @return the maximum degree range for this coordinate
199 protected abstract int getMaxDegrees();
204 * @param inValue value of coordinate
205 * @param inFormat format to use
206 * @param inCardinal cardinal
208 protected Coordinate(double inValue, int inFormat, int inCardinal)
211 // Calculate degrees, minutes, seconds
212 _degrees = (int) Math.abs(inValue);
213 double numMins = (Math.abs(_asDouble)-_degrees) * 60.0;
214 _minutes = (int) numMins;
215 double numSecs = (numMins - _minutes) * 60.0;
216 _seconds = (int) numSecs;
217 _fracs = (int) ((numSecs - _seconds) * 10);
218 _fracDenom = 10; // fixed for now
219 // Make a string to display on screen
220 _cardinal = inCardinal;
221 _originalFormat = FORMAT_NONE;
222 if (inFormat == FORMAT_NONE) inFormat = FORMAT_DEG_WITHOUT_CARDINAL;
223 _originalString = output(inFormat);
224 _originalFormat = inFormat;
230 * @return coordinate as a double
232 public double getDouble()
238 * @return true if Coordinate is valid
240 public boolean isValid()
246 * Compares two Coordinates for equality
247 * @param inOther other Coordinate object with which to compare
248 * @return true if the two objects are equal
250 public boolean equals(Coordinate inOther)
252 return (inOther != null && _cardinal == inOther._cardinal
253 && _degrees == inOther._degrees
254 && _minutes == inOther._minutes
255 && _seconds == inOther._seconds
256 && _fracs == inOther._fracs);
261 * Output the Coordinate in the given format
262 * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
263 * @return String for output
265 public String output(int inFormat)
267 String answer = _originalString;
268 if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
270 // TODO: allow specification of precision for output of d-m and d
271 // format as specified
274 case FORMAT_DEG_MIN_SEC:
276 StringBuffer buffer = new StringBuffer();
277 buffer.append(PRINTABLE_CARDINALS[_cardinal])
278 .append(threeDigitString(_degrees)).append('\u00B0')
279 .append(twoDigitString(_minutes)).append('\'')
280 .append(twoDigitString(_seconds)).append('.')
281 .append(formatFraction(_fracs, _fracDenom));
282 answer = buffer.toString();
287 answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "\u00B0"
288 + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
291 case FORMAT_DEG_WHOLE_MIN:
293 answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "\u00B0"
294 + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5) + "'";
298 case FORMAT_DEG_WITHOUT_CARDINAL:
300 answer = (_asDouble<0.0?"-":"")
301 + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
304 case FORMAT_DECIMAL_FORCE_POINT:
306 // Forcing a decimal point instead of system-dependent commas etc
307 answer = EIGHT_DP.format(_asDouble);
310 case FORMAT_DEG_MIN_SEC_WITH_SPACES:
312 // Note: cardinal not needed as this format is only for exif, which has cardinal separately
313 answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
316 case FORMAT_CARDINAL:
318 answer = "" + PRINTABLE_CARDINALS[_cardinal];
327 * Format the fraction part of seconds value
328 * @param inFrac fractional part eg 123
329 * @param inDenom denominator of fraction eg 10000
330 * @return String describing fraction, in this case 0123
332 private static final String formatFraction(int inFrac, int inDenom)
334 if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
335 String denomString = "" + inDenom;
336 int reqdLen = denomString.length() - 1;
337 String result = denomString + inFrac;
338 int resultLen = result.length();
339 return result.substring(resultLen - reqdLen);
344 * Format an integer to a two-digit String
345 * @param inNumber number to format
346 * @return two-character String
348 private static String twoDigitString(int inNumber)
350 if (inNumber <= 0) return "00";
351 if (inNumber < 10) return "0" + inNumber;
352 if (inNumber < 100) return "" + inNumber;
353 return "" + (inNumber % 100);
358 * Format an integer to a three-digit String for degrees
359 * @param inNumber number to format
360 * @return three-character String
362 private static String threeDigitString(int inNumber)
364 if (inNumber <= 0) return "000";
365 if (inNumber < 10) return "00" + inNumber;
366 if (inNumber < 100) return "0" + inNumber;
367 return "" + (inNumber % 1000);
372 * Create a new Coordinate between two others
373 * @param inStart start coordinate
374 * @param inEnd end coordinate
375 * @param inIndex index of point
376 * @param inNumPoints number of points to interpolate
377 * @return new Coordinate object
379 public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
380 int inIndex, int inNumPoints)
382 return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
387 * Create a new Coordinate between two others
388 * @param inStart start coordinate
389 * @param inEnd end coordinate
390 * @param inFraction fraction from start to end
391 * @return new Coordinate object
393 public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
396 double startValue = inStart.getDouble();
397 double endValue = inEnd.getDouble();
398 double newValue = startValue + (endValue - startValue) * inFraction;
399 Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat);
405 * Make a new Coordinate according to subclass
406 * @param inValue double value
407 * @param inFormat format to use
408 * @return object of Coordinate subclass
410 protected abstract Coordinate makeNew(double inValue, int inFormat);
414 * Create a String representation for debug
415 * @return String describing coordinate value
417 public String toString()
419 return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
420 + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;