]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/data/Coordinate.java
Version 13, August 2011
[GpsPrune.git] / tim / prune / data / Coordinate.java
1 package tim.prune.data;
2
3 import java.text.DecimalFormat;
4 import java.text.NumberFormat;
5 import java.util.Locale;
6
7 /**
8  * Class to represent a lat/long coordinate
9  * and provide conversion functions
10  */
11 public abstract class Coordinate
12 {
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;
28
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)
32         static {
33                 if (EIGHT_DP instanceof DecimalFormat) ((DecimalFormat) EIGHT_DP).applyPattern("0.00000000");
34         }
35
36         // Instance variables
37         private boolean _valid = false;
38         private boolean _cardinalGuessed = false;
39         protected int _cardinal = NORTH;
40         private int _degrees = 0;
41         private int _minutes = 0;
42         private int _seconds = 0;
43         private int _fracs = 0;
44         private int _fracDenom = 0;
45         private String _originalString = null;
46         private int _originalFormat = FORMAT_NONE;
47         private double _asDouble = 0.0;
48
49
50         /**
51          * Constructor given String
52          * @param inString string to parse
53          */
54         public Coordinate(String inString)
55         {
56                 _originalString = inString;
57                 int strLen = 0;
58                 if (inString != null)
59                 {
60                         inString = inString.trim();
61                         strLen = inString.length();
62                 }
63                 if (strLen > 0)
64                 {
65                         // Check for cardinal character either at beginning or end
66                         boolean hasCardinal = true;
67                         _cardinal = getCardinal(inString.charAt(0), inString.charAt(strLen-1));
68                         if (_cardinal == NO_CARDINAL) {
69                                 hasCardinal = false;
70                                 // use default from concrete subclass
71                                 _cardinal = getDefaultCardinal();
72                                 _cardinalGuessed = true;
73                         }
74                         else if (isJustNumber(inString)) {
75                                 // it's just a number
76                                 hasCardinal = false;
77                                 _cardinalGuessed = true;
78                         }
79
80                         // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
81                         int numFields = 0;
82                         boolean inNumeric = false;
83                         char currChar;
84                         long[] fields = new long[4]; // needs to be long for lengthy decimals
85                         long[] denoms = new long[4];
86                         boolean[] otherDelims = new boolean[5]; // remember whether delimiters have non-decimal chars
87                         try
88                         {
89                                 // Loop over characters in input string, populating fields array
90                                 for (int i=0; i<strLen; i++)
91                                 {
92                                         currChar = inString.charAt(i);
93                                         if (currChar >= '0' && currChar <= '9')
94                                         {
95                                                 if (!inNumeric)
96                                                 {
97                                                         inNumeric = true;
98                                                         numFields++;
99                                                         denoms[numFields-1] = 1;
100                                                 }
101                                                 if (denoms[numFields-1] < 1E18) // ignore trailing characters if too big for long
102                                                 {
103                                                         fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0');
104                                                         denoms[numFields-1] *= 10;
105                                                 }
106                                         }
107                                         else
108                                         {
109                                                 inNumeric = false;
110                                                 // Remember delimiters
111                                                 if (currChar != ',' && currChar != '.') {otherDelims[numFields] = true;}
112                                         }
113                                 }
114                                 _valid = (numFields > 0);
115                         }
116                         catch (ArrayIndexOutOfBoundsException obe)
117                         {
118                                 // more than four fields found - unable to parse
119                                 _valid = false;
120                         }
121                         // parse fields according to number found
122                         _degrees = (int) fields[0];
123                         _asDouble = _degrees;
124                         _originalFormat = hasCardinal?FORMAT_DEG:FORMAT_DEG_WITHOUT_CARDINAL;
125                         _fracDenom = 10;
126                         if (numFields == 2)
127                         {
128                                 if (!otherDelims[1])
129                                 {
130                                         // String is just decimal degrees
131                                         double numMins = fields[1] * 60.0 / denoms[1];
132                                         _minutes = (int) numMins;
133                                         double numSecs = (numMins - _minutes) * 60.0;
134                                         _seconds = (int) numSecs;
135                                         _fracs = (int) ((numSecs - _seconds) * 10);
136                                         _asDouble = _degrees + 1.0 * fields[1] / denoms[1];
137                                 }
138                                 else
139                                 {
140                                         // String is degrees and minutes (due to non-decimal separator)
141                                         _originalFormat = FORMAT_DEG_MIN;
142                                         _minutes = (int) fields[1];
143                                         _seconds = 0;
144                                         _fracs = 0;
145                                         _asDouble = 1.0 * _degrees + (_minutes / 60.0);
146                                 }
147                         }
148                         // Differentiate between d-m.f and d-m-s using . or ,
149                         else if (numFields == 3 && !otherDelims[2])
150                         {
151                                 // String is degrees-minutes.fractions
152                                 _originalFormat = FORMAT_DEG_MIN;
153                                 _minutes = (int) fields[1];
154                                 double numSecs = fields[2] * 60.0 / denoms[2];
155                                 _seconds = (int) numSecs;
156                                 _fracs = (int) ((numSecs - _seconds) * 10);
157                                 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (numSecs / 3600.0);
158                         }
159                         else if (numFields == 4 || numFields == 3)
160                         {
161                                 // String is degrees-minutes-seconds.fractions
162                                 _originalFormat = FORMAT_DEG_MIN_SEC;
163                                 _minutes = (int) fields[1];
164                                 _seconds = (int) fields[2];
165                                 _fracs = (int) fields[3];
166                                 _fracDenom = (int) denoms[3];
167                                 if (_fracDenom < 1) {_fracDenom = 1;}
168                                 _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
169                         }
170                         if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
171                                 _asDouble = -_asDouble;
172                         // validate fields
173                         _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
174                 }
175                 else _valid = false;
176         }
177
178
179         /**
180          * Get the cardinal from the given character
181          * @param inFirstChar first character from file
182          * @param inLastChar last character from file
183          */
184         protected int getCardinal(char inFirstChar, char inLastChar)
185         {
186                 // Try leading character first
187                 int cardinal = getCardinal(inFirstChar);
188                 // if not there, try trailing character
189                 if (cardinal == NO_CARDINAL) {
190                         cardinal = getCardinal(inLastChar);
191                 }
192                 return cardinal;
193         }
194
195         /**
196          * @return true if cardinal was guessed, false if parsed
197          */
198         public boolean getCardinalGuessed() {
199                 return _cardinalGuessed;
200         }
201
202         /**
203          * Get the cardinal from the given character
204          * @param inChar character from file
205          */
206         protected abstract int getCardinal(char inChar);
207
208         /**
209          * @return the default cardinal for the subclass
210          */
211         protected abstract int getDefaultCardinal();
212
213         /**
214          * @return the maximum degree range for this coordinate
215          */
216         protected abstract int getMaxDegrees();
217
218
219         /**
220          * Constructor
221          * @param inValue value of coordinate
222          * @param inFormat format to use
223          * @param inCardinal cardinal
224          */
225         protected Coordinate(double inValue, int inFormat, int inCardinal)
226         {
227                 _asDouble = inValue;
228                 // Calculate degrees, minutes, seconds
229                 _degrees = (int) Math.abs(inValue);
230                 double numMins = (Math.abs(_asDouble)-_degrees) * 60.0;
231                 _minutes = (int) numMins;
232                 double numSecs = (numMins - _minutes) * 60.0;
233                 _seconds = (int) numSecs;
234                 _fracs = (int) ((numSecs - _seconds) * 10);
235                 _fracDenom = 10; // fixed for now
236                 // Make a string to display on screen
237                 _cardinal = inCardinal;
238                 _originalFormat = FORMAT_NONE;
239                 if (inFormat == FORMAT_NONE) inFormat = FORMAT_DEG_WITHOUT_CARDINAL;
240                 _originalString = output(inFormat);
241                 _originalFormat = inFormat;
242                 _valid = true;
243         }
244
245
246         /**
247          * @return coordinate as a double
248          */
249         public double getDouble()
250         {
251                 return _asDouble;
252         }
253
254         /**
255          * @return true if Coordinate is valid
256          */
257         public boolean isValid()
258         {
259                 return _valid;
260         }
261
262         /**
263          * Compares two Coordinates for equality
264          * @param inOther other Coordinate object with which to compare
265          * @return true if the two objects are equal
266          */
267         public boolean equals(Coordinate inOther)
268         {
269                 return (_asDouble == inOther._asDouble);
270         }
271
272
273         /**
274          * Output the Coordinate in the given format
275          * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
276          * @return String for output
277          */
278         public String output(int inFormat)
279         {
280                 String answer = _originalString;
281                 if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
282                 {
283                         // TODO: allow specification of precision for output of d-m and d
284                         // format as specified
285                         switch (inFormat)
286                         {
287                                 case FORMAT_DEG_MIN_SEC:
288                                 {
289                                         StringBuffer buffer = new StringBuffer();
290                                         buffer.append(PRINTABLE_CARDINALS[_cardinal])
291                                                 .append(threeDigitString(_degrees)).append('\u00B0')
292                                                 .append(twoDigitString(_minutes)).append('\'')
293                                                 .append(twoDigitString(_seconds)).append('.')
294                                                 .append(formatFraction(_fracs, _fracDenom));
295                                         answer = buffer.toString();
296                                         break;
297                                 }
298                                 case FORMAT_DEG_MIN:
299                                 {
300                                         answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "\u00B0"
301                                                 + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
302                                         break;
303                                 }
304                                 case FORMAT_DEG_WHOLE_MIN:
305                                 {
306                                         int deg = _degrees;
307                                         int min = (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5);
308                                         if (min == 60) {
309                                                 min = 0; deg++;
310                                         }
311                                         answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(deg) + "\u00B0" + min + "'";
312                                         break;
313                                 }
314                                 case FORMAT_DEG:
315                                 case FORMAT_DEG_WITHOUT_CARDINAL:
316                                 {
317                                         answer = (_asDouble<0.0?"-":"")
318                                                 + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
319                                         break;
320                                 }
321                                 case FORMAT_DECIMAL_FORCE_POINT:
322                                 {
323                                         // Forcing a decimal point instead of system-dependent commas etc
324                                         if (_originalFormat != FORMAT_DEG_WITHOUT_CARDINAL || answer.indexOf('.') < 0) {
325                                                 answer = EIGHT_DP.format(_asDouble);
326                                         }
327                                         break;
328                                 }
329                                 case FORMAT_DEG_MIN_SEC_WITH_SPACES:
330                                 {
331                                         // Note: cardinal not needed as this format is only for exif, which has cardinal separately
332                                         answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
333                                         break;
334                                 }
335                                 case FORMAT_CARDINAL:
336                                 {
337                                         answer = "" + PRINTABLE_CARDINALS[_cardinal];
338                                         break;
339                                 }
340                         }
341                 }
342                 return answer;
343         }
344
345         /**
346          * Format the fraction part of seconds value
347          * @param inFrac fractional part eg 123
348          * @param inDenom denominator of fraction eg 10000
349          * @return String describing fraction, in this case 0123
350          */
351         private static final String formatFraction(int inFrac, int inDenom)
352         {
353                 if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
354                 String denomString = "" + inDenom;
355                 int reqdLen = denomString.length() - 1;
356                 String result = denomString + inFrac;
357                 int resultLen = result.length();
358                 return result.substring(resultLen - reqdLen);
359         }
360
361
362         /**
363          * Format an integer to a two-digit String
364          * @param inNumber number to format
365          * @return two-character String
366          */
367         private static String twoDigitString(int inNumber)
368         {
369                 if (inNumber <= 0) return "00";
370                 if (inNumber < 10) return "0" + inNumber;
371                 if (inNumber < 100) return "" + inNumber;
372                 return "" + (inNumber % 100);
373         }
374
375
376         /**
377          * Format an integer to a three-digit String for degrees
378          * @param inNumber number to format
379          * @return three-character String
380          */
381         private static String threeDigitString(int inNumber)
382         {
383                 if (inNumber <= 0) return "000";
384                 if (inNumber < 10) return "00" + inNumber;
385                 if (inNumber < 100) return "0" + inNumber;
386                 return "" + (inNumber % 1000);
387         }
388
389
390         /**
391          * Create a new Coordinate between two others
392          * @param inStart start coordinate
393          * @param inEnd end coordinate
394          * @param inIndex index of point
395          * @param inNumPoints number of points to interpolate
396          * @return new Coordinate object
397          */
398         public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
399                 int inIndex, int inNumPoints)
400         {
401                 return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
402         }
403
404
405         /**
406          * Create a new Coordinate between two others
407          * @param inStart start coordinate
408          * @param inEnd end coordinate
409          * @param inFraction fraction from start to end
410          * @return new Coordinate object
411          */
412         public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
413                 double inFraction)
414         {
415                 double startValue = inStart.getDouble();
416                 double endValue = inEnd.getDouble();
417                 double newValue = startValue + (endValue - startValue) * inFraction;
418                 Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat);
419                 return answer;
420         }
421
422
423         /**
424          * Make a new Coordinate according to subclass
425          * @param inValue double value
426          * @param inFormat format to use
427          * @return object of Coordinate subclass
428          */
429         protected abstract Coordinate makeNew(double inValue, int inFormat);
430
431         /**
432          * Try to parse the given string
433          * @param inString string to check
434          * @return true if it can be parsed as a number
435          */
436         private static boolean isJustNumber(String inString)
437         {
438                 boolean justNum = false;
439                 try {
440                         double x = Double.parseDouble(inString);
441                         justNum = (x >= -180.0 && x <= 360.0);
442                 }
443                 catch (NumberFormatException nfe) {} // flag remains false
444                 return justNum;
445         }
446
447         /**
448          * Create a String representation for debug
449          * @return String describing coordinate value
450          */
451         public String toString()
452         {
453                 return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
454                         + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;
455         }
456 }