1 package tim.prune.data;
4 * Class to represent a lat/long coordinate
5 * and provide conversion functions
7 public abstract class Coordinate
9 public static final int NO_CARDINAL = -1;
10 public static final int NORTH = 0;
11 public static final int EAST = 1;
12 public static final int SOUTH = 2;
13 public static final int WEST = 3;
14 public static final char[] PRINTABLE_CARDINALS = {'N', 'E', 'S', 'W'};
15 public static final int FORMAT_DEG_MIN_SEC = 10;
16 public static final int FORMAT_DEG_MIN = 11;
17 public static final int FORMAT_DEG = 12;
18 public static final int FORMAT_DEG_WITHOUT_CARDINAL = 13;
19 public static final int FORMAT_DEG_WHOLE_MIN = 14;
20 public static final int FORMAT_DEG_MIN_SEC_WITH_SPACES = 15;
21 public static final int FORMAT_CARDINAL = 16;
22 public static final int FORMAT_NONE = 19;
25 private boolean _valid = false;
26 protected int _cardinal = NORTH;
27 private int _degrees = 0;
28 private int _minutes = 0;
29 private int _seconds = 0;
30 private int _fracs = 0;
31 private int _fracDenom = 0;
32 private String _originalString = null;
33 private int _originalFormat = FORMAT_NONE;
34 private double _asDouble = 0.0;
38 * Constructor given String
39 * @param inString string to parse
41 public Coordinate(String inString)
43 _originalString = inString;
47 inString = inString.trim();
48 strLen = inString.length();
52 // Check for cardinal character either at beginning or end
53 boolean hasCardinal = true;
54 _cardinal = getCardinal(inString.charAt(0), inString.charAt(strLen-1));
55 if (_cardinal == NO_CARDINAL) {
57 // use default from concrete subclass
58 _cardinal = getDefaultCardinal();
61 // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
63 boolean inNumeric = false;
65 long[] fields = new long[4]; // needs to be long for lengthy decimals
66 long[] denoms = new long[4];
67 String secondDelim = "";
70 // Loop over characters in input string, populating fields array
71 for (int i=0; i<strLen; i++)
73 currChar = inString.charAt(i);
74 if (currChar >= '0' && currChar <= '9')
80 denoms[numFields-1] = 1;
82 fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0');
83 denoms[numFields-1] *= 10;
88 // Remember second delimiter
90 secondDelim += currChar;
94 _valid = (numFields > 0);
96 catch (ArrayIndexOutOfBoundsException obe)
98 // more than four fields found - unable to parse
101 // parse fields according to number found
102 _degrees = (int) fields[0];
103 _originalFormat = hasCardinal?FORMAT_DEG:FORMAT_DEG_WITHOUT_CARDINAL;
107 // String is just decimal degrees
108 double numMins = fields[1] * 60.0 / denoms[1];
109 _minutes = (int) numMins;
110 double numSecs = (numMins - _minutes) * 60.0;
111 _seconds = (int) numSecs;
112 _fracs = (int) ((numSecs - _seconds) * 10);
114 // Differentiate between d-m.f and d-m-s using . or ,
115 else if (numFields == 3 && (secondDelim.equals(".") || secondDelim.equals(",")))
117 // String is degrees-minutes.fractions
118 _originalFormat = FORMAT_DEG_MIN;
119 _minutes = (int) fields[1];
120 double numSecs = fields[2] * 60.0 / denoms[2];
121 _seconds = (int) numSecs;
122 _fracs = (int) ((numSecs - _seconds) * 10);
124 else if (numFields == 4 || numFields == 3)
126 // String is degrees-minutes-seconds.fractions
127 _originalFormat = FORMAT_DEG_MIN_SEC;
128 _minutes = (int) fields[1];
129 _seconds = (int) fields[2];
130 _fracs = (int) fields[3];
131 _fracDenom = (int) denoms[3];
132 if (_fracDenom < 1) {_fracDenom = 1;}
134 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
135 if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
136 _asDouble = -_asDouble;
138 _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
145 * Get the cardinal from the given character
146 * @param inFirstChar first character from file
147 * @param inLastChar last character from file
149 protected int getCardinal(char inFirstChar, char inLastChar)
151 // Try leading character first
152 int cardinal = getCardinal(inFirstChar);
153 // if not there, try trailing character
154 if (cardinal == NO_CARDINAL) {
155 cardinal = getCardinal(inLastChar);
162 * Get the cardinal from the given character
163 * @param inChar character from file
165 protected abstract int getCardinal(char inChar);
168 * @return the default cardinal for the subclass
170 protected abstract int getDefaultCardinal();
173 * @return the maximum degree range for this coordinate
175 protected abstract int getMaxDegrees();
180 * @param inValue value of coordinate
181 * @param inFormat format to use
182 * @param inCardinal cardinal
184 protected Coordinate(double inValue, int inFormat, int inCardinal)
187 // Calculate degrees, minutes, seconds
188 _degrees = (int) Math.abs(inValue);
189 double numMins = (Math.abs(_asDouble)-_degrees) * 60.0;
190 _minutes = (int) numMins;
191 double numSecs = (numMins - _minutes) * 60.0;
192 _seconds = (int) numSecs;
193 _fracs = (int) ((numSecs - _seconds) * 10);
194 _fracDenom = 10; // fixed for now
195 // Make a string to display on screen
196 _cardinal = inCardinal;
197 _originalFormat = FORMAT_NONE;
198 if (inFormat == FORMAT_NONE) inFormat = FORMAT_DEG_WITHOUT_CARDINAL;
199 _originalString = output(inFormat);
200 _originalFormat = inFormat;
206 * @return coordinate as a double
208 public double getDouble()
214 * @return true if Coordinate is valid
216 public boolean isValid()
222 * Compares two Coordinates for equality
223 * @param inOther other Coordinate object with which to compare
224 * @return true if the two objects are equal
226 public boolean equals(Coordinate inOther)
228 return (inOther != null && _cardinal == inOther._cardinal
229 && _degrees == inOther._degrees
230 && _minutes == inOther._minutes
231 && _seconds == inOther._seconds
232 && _fracs == inOther._fracs);
237 * Output the Coordinate in the given format
238 * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
239 * @return String for output
241 public String output(int inFormat)
243 String answer = _originalString;
244 if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
246 // TODO: allow specification of precision for output of d-m and d
247 // format as specified
250 case FORMAT_DEG_MIN_SEC:
252 StringBuffer buffer = new StringBuffer();
253 buffer.append(PRINTABLE_CARDINALS[_cardinal])
254 .append(threeDigitString(_degrees)).append('°')
255 .append(twoDigitString(_minutes)).append('\'')
256 .append(twoDigitString(_seconds)).append('.')
257 .append(formatFraction(_fracs, _fracDenom));
258 answer = buffer.toString();
263 answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
264 + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
267 case FORMAT_DEG_WHOLE_MIN:
269 answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
270 + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5) + "'";
274 case FORMAT_DEG_WITHOUT_CARDINAL:
276 answer = (_asDouble<0.0?"-":"")
277 + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
280 case FORMAT_DEG_MIN_SEC_WITH_SPACES:
282 // Note: cardinal not needed as this format is only for exif, which has cardinal separately
283 answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
286 case FORMAT_CARDINAL:
288 answer = "" + PRINTABLE_CARDINALS[_cardinal];
297 * Format the fraction part of seconds value
298 * @param inFrac fractional part eg 123
299 * @param inDenom denominator of fraction eg 10000
300 * @return String describing fraction, in this case 0123
302 private static final String formatFraction(int inFrac, int inDenom)
304 if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
305 String denomString = "" + inDenom;
306 int reqdLen = denomString.length() - 1;
307 String result = denomString + inFrac;
308 int resultLen = result.length();
309 return result.substring(resultLen - reqdLen);
314 * Format an integer to a two-digit String
315 * @param inNumber number to format
316 * @return two-character String
318 private static String twoDigitString(int inNumber)
320 if (inNumber <= 0) return "00";
321 if (inNumber < 10) return "0" + inNumber;
322 if (inNumber < 100) return "" + inNumber;
323 return "" + (inNumber % 100);
328 * Format an integer to a three-digit String for degrees
329 * @param inNumber number to format
330 * @return three-character String
332 private static String threeDigitString(int inNumber)
334 if (inNumber <= 0) return "000";
335 if (inNumber < 10) return "00" + inNumber;
336 if (inNumber < 100) return "0" + inNumber;
337 return "" + (inNumber % 1000);
342 * Create a new Coordinate between two others
343 * @param inStart start coordinate
344 * @param inEnd end coordinate
345 * @param inIndex index of point
346 * @param inNumPoints number of points to interpolate
347 * @return new Coordinate object
349 public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
350 int inIndex, int inNumPoints)
352 return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
357 * Create a new Coordinate between two others
358 * @param inStart start coordinate
359 * @param inEnd end coordinate
360 * @param inFraction fraction from start to end
361 * @return new Coordinate object
363 public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
366 double startValue = inStart.getDouble();
367 double endValue = inEnd.getDouble();
368 double newValue = startValue + (endValue - startValue) * inFraction;
369 Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat);
375 * Make a new Coordinate according to subclass
376 * @param inValue double value
377 * @param inFormat format to use
378 * @return object of Coordinate subclass
380 protected abstract Coordinate makeNew(double inValue, int inFormat);
384 * Create a String representation for debug
385 * @return String describing coordinate value
387 public String toString()
389 return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
390 + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;