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 String secondDelim = "";
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 second delimiter
101 if (numFields == 2) {
102 secondDelim += currChar;
106 _valid = (numFields > 0);
108 catch (ArrayIndexOutOfBoundsException obe)
110 // more than four fields found - unable to parse
113 // parse fields according to number found
114 _degrees = (int) fields[0];
115 _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];
127 // Differentiate between d-m.f and d-m-s using . or ,
128 else if (numFields == 3 && (secondDelim.equals(".") || secondDelim.equals(",")))
130 // String is degrees-minutes.fractions
131 _originalFormat = FORMAT_DEG_MIN;
132 _minutes = (int) fields[1];
133 double numSecs = fields[2] * 60.0 / denoms[2];
134 _seconds = (int) numSecs;
135 _fracs = (int) ((numSecs - _seconds) * 10);
136 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (numSecs / 3600.0);
138 else if (numFields == 4 || numFields == 3)
140 // String is degrees-minutes-seconds.fractions
141 _originalFormat = FORMAT_DEG_MIN_SEC;
142 _minutes = (int) fields[1];
143 _seconds = (int) fields[2];
144 _fracs = (int) fields[3];
145 _fracDenom = (int) denoms[3];
146 if (_fracDenom < 1) {_fracDenom = 1;}
147 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
149 if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
150 _asDouble = -_asDouble;
152 _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
159 * Get the cardinal from the given character
160 * @param inFirstChar first character from file
161 * @param inLastChar last character from file
163 protected int getCardinal(char inFirstChar, char inLastChar)
165 // Try leading character first
166 int cardinal = getCardinal(inFirstChar);
167 // if not there, try trailing character
168 if (cardinal == NO_CARDINAL) {
169 cardinal = getCardinal(inLastChar);
176 * Get the cardinal from the given character
177 * @param inChar character from file
179 protected abstract int getCardinal(char inChar);
182 * @return the default cardinal for the subclass
184 protected abstract int getDefaultCardinal();
187 * @return the maximum degree range for this coordinate
189 protected abstract int getMaxDegrees();
194 * @param inValue value of coordinate
195 * @param inFormat format to use
196 * @param inCardinal cardinal
198 protected Coordinate(double inValue, int inFormat, int inCardinal)
201 // Calculate degrees, minutes, seconds
202 _degrees = (int) Math.abs(inValue);
203 double numMins = (Math.abs(_asDouble)-_degrees) * 60.0;
204 _minutes = (int) numMins;
205 double numSecs = (numMins - _minutes) * 60.0;
206 _seconds = (int) numSecs;
207 _fracs = (int) ((numSecs - _seconds) * 10);
208 _fracDenom = 10; // fixed for now
209 // Make a string to display on screen
210 _cardinal = inCardinal;
211 _originalFormat = FORMAT_NONE;
212 if (inFormat == FORMAT_NONE) inFormat = FORMAT_DEG_WITHOUT_CARDINAL;
213 _originalString = output(inFormat);
214 _originalFormat = inFormat;
220 * @return coordinate as a double
222 public double getDouble()
228 * @return true if Coordinate is valid
230 public boolean isValid()
236 * Compares two Coordinates for equality
237 * @param inOther other Coordinate object with which to compare
238 * @return true if the two objects are equal
240 public boolean equals(Coordinate inOther)
242 return (inOther != null && _cardinal == inOther._cardinal
243 && _degrees == inOther._degrees
244 && _minutes == inOther._minutes
245 && _seconds == inOther._seconds
246 && _fracs == inOther._fracs);
251 * Output the Coordinate in the given format
252 * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
253 * @return String for output
255 public String output(int inFormat)
257 String answer = _originalString;
258 if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
260 // TODO: allow specification of precision for output of d-m and d
261 // format as specified
264 case FORMAT_DEG_MIN_SEC:
266 StringBuffer buffer = new StringBuffer();
267 buffer.append(PRINTABLE_CARDINALS[_cardinal])
268 .append(threeDigitString(_degrees)).append('°')
269 .append(twoDigitString(_minutes)).append('\'')
270 .append(twoDigitString(_seconds)).append('.')
271 .append(formatFraction(_fracs, _fracDenom));
272 answer = buffer.toString();
277 answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
278 + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
281 case FORMAT_DEG_WHOLE_MIN:
283 answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
284 + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5) + "'";
288 case FORMAT_DEG_WITHOUT_CARDINAL:
290 answer = (_asDouble<0.0?"-":"")
291 + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
294 case FORMAT_DECIMAL_FORCE_POINT:
296 // Forcing a decimal point instead of system-dependent commas etc
297 answer = EIGHT_DP.format(_asDouble);
300 case FORMAT_DEG_MIN_SEC_WITH_SPACES:
302 // Note: cardinal not needed as this format is only for exif, which has cardinal separately
303 answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
306 case FORMAT_CARDINAL:
308 answer = "" + PRINTABLE_CARDINALS[_cardinal];
317 * Format the fraction part of seconds value
318 * @param inFrac fractional part eg 123
319 * @param inDenom denominator of fraction eg 10000
320 * @return String describing fraction, in this case 0123
322 private static final String formatFraction(int inFrac, int inDenom)
324 if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
325 String denomString = "" + inDenom;
326 int reqdLen = denomString.length() - 1;
327 String result = denomString + inFrac;
328 int resultLen = result.length();
329 return result.substring(resultLen - reqdLen);
334 * Format an integer to a two-digit String
335 * @param inNumber number to format
336 * @return two-character String
338 private static String twoDigitString(int inNumber)
340 if (inNumber <= 0) return "00";
341 if (inNumber < 10) return "0" + inNumber;
342 if (inNumber < 100) return "" + inNumber;
343 return "" + (inNumber % 100);
348 * Format an integer to a three-digit String for degrees
349 * @param inNumber number to format
350 * @return three-character String
352 private static String threeDigitString(int inNumber)
354 if (inNumber <= 0) return "000";
355 if (inNumber < 10) return "00" + inNumber;
356 if (inNumber < 100) return "0" + inNumber;
357 return "" + (inNumber % 1000);
362 * Create a new Coordinate between two others
363 * @param inStart start coordinate
364 * @param inEnd end coordinate
365 * @param inIndex index of point
366 * @param inNumPoints number of points to interpolate
367 * @return new Coordinate object
369 public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
370 int inIndex, int inNumPoints)
372 return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
377 * Create a new Coordinate between two others
378 * @param inStart start coordinate
379 * @param inEnd end coordinate
380 * @param inFraction fraction from start to end
381 * @return new Coordinate object
383 public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
386 double startValue = inStart.getDouble();
387 double endValue = inEnd.getDouble();
388 double newValue = startValue + (endValue - startValue) * inFraction;
389 Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat);
395 * Make a new Coordinate according to subclass
396 * @param inValue double value
397 * @param inFormat format to use
398 * @return object of Coordinate subclass
400 protected abstract Coordinate makeNew(double inValue, int inFormat);
404 * Create a String representation for debug
405 * @return String describing coordinate value
407 public String toString()
409 return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
410 + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;