]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/data/Coordinate.java
Version 8, September 2009
[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         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;
47
48
49         /**
50          * Constructor given String
51          * @param inString string to parse
52          */
53         public Coordinate(String inString)
54         {
55                 _originalString = inString;
56                 int strLen = 0;
57                 if (inString != null)
58                 {
59                         inString = inString.trim();
60                         strLen = inString.length();
61                 }
62                 if (strLen > 1)
63                 {
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) {
68                                 hasCardinal = false;
69                                 // use default from concrete subclass
70                                 _cardinal = getDefaultCardinal();
71                         }
72
73                         // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
74                         int numFields = 0;
75                         boolean inNumeric = false;
76                         char currChar;
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
80                         try
81                         {
82                                 // Loop over characters in input string, populating fields array
83                                 for (int i=0; i<strLen; i++)
84                                 {
85                                         currChar = inString.charAt(i);
86                                         if (currChar >= '0' && currChar <= '9')
87                                         {
88                                                 if (!inNumeric)
89                                                 {
90                                                         inNumeric = true;
91                                                         numFields++;
92                                                         denoms[numFields-1] = 1;
93                                                 }
94                                                 fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0');
95                                                 denoms[numFields-1] *= 10;
96                                         }
97                                         else
98                                         {
99                                                 inNumeric = false;
100                                                 // Remember delimiters
101                                                 if (currChar != ',' && currChar != '.') {otherDelims[numFields] = true;}
102                                         }
103                                 }
104                                 _valid = (numFields > 0);
105                         }
106                         catch (ArrayIndexOutOfBoundsException obe)
107                         {
108                                 // more than four fields found - unable to parse
109                                 _valid = false;
110                         }
111                         // parse fields according to number found
112                         _degrees = (int) fields[0];
113                         _originalFormat = hasCardinal?FORMAT_DEG:FORMAT_DEG_WITHOUT_CARDINAL;
114                         _fracDenom = 10;
115                         if (numFields == 2)
116                         {
117                                 if (!otherDelims[1])
118                                 {
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];
126                                 }
127                                 else
128                                 {
129                                         // String is degrees and minutes (due to non-decimal separator)
130                                         _originalFormat = FORMAT_DEG_MIN;
131                                         _minutes = (int) fields[1];
132                                         _seconds = 0;
133                                         _fracs = 0;
134                                         _asDouble = 1.0 * _degrees + (_minutes / 60.0);
135                                 }
136                         }
137                         // Differentiate between d-m.f and d-m-s using . or ,
138                         else if (numFields == 3 && !otherDelims[2])
139                         {
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);
147                         }
148                         else if (numFields == 4 || numFields == 3)
149                         {
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);
158                         }
159                         if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
160                                 _asDouble = -_asDouble;
161                         // validate fields
162                         _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
163                 }
164                 else _valid = false;
165         }
166
167
168         /**
169          * Get the cardinal from the given character
170          * @param inFirstChar first character from file
171          * @param inLastChar last character from file
172          */
173         protected int getCardinal(char inFirstChar, char inLastChar)
174         {
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);
180                 }
181                 return cardinal;
182         }
183
184
185         /**
186          * Get the cardinal from the given character
187          * @param inChar character from file
188          */
189         protected abstract int getCardinal(char inChar);
190
191         /**
192          * @return the default cardinal for the subclass
193          */
194         protected abstract int getDefaultCardinal();
195
196         /**
197          * @return the maximum degree range for this coordinate
198          */
199         protected abstract int getMaxDegrees();
200
201
202         /**
203          * Constructor
204          * @param inValue value of coordinate
205          * @param inFormat format to use
206          * @param inCardinal cardinal
207          */
208         protected Coordinate(double inValue, int inFormat, int inCardinal)
209         {
210                 _asDouble = inValue;
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;
225                 _valid = true;
226         }
227
228
229         /**
230          * @return coordinate as a double
231          */
232         public double getDouble()
233         {
234                 return _asDouble;
235         }
236
237         /**
238          * @return true if Coordinate is valid
239          */
240         public boolean isValid()
241         {
242                 return _valid;
243         }
244
245         /**
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
249          */
250         public boolean equals(Coordinate inOther)
251         {
252                 return (inOther != null && _cardinal == inOther._cardinal
253                         && _degrees == inOther._degrees
254                         && _minutes == inOther._minutes
255                         && _seconds == inOther._seconds
256                         && _fracs == inOther._fracs);
257         }
258
259
260         /**
261          * Output the Coordinate in the given format
262          * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
263          * @return String for output
264          */
265         public String output(int inFormat)
266         {
267                 String answer = _originalString;
268                 if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
269                 {
270                         // TODO: allow specification of precision for output of d-m and d
271                         // format as specified
272                         switch (inFormat)
273                         {
274                                 case FORMAT_DEG_MIN_SEC:
275                                 {
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();
283                                         break;
284                                 }
285                                 case FORMAT_DEG_MIN:
286                                 {
287                                         answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "\u00B0"
288                                                 + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
289                                         break;
290                                 }
291                                 case FORMAT_DEG_WHOLE_MIN:
292                                 {
293                                         answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "\u00B0"
294                                                 + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5) + "'";
295                                         break;
296                                 }
297                                 case FORMAT_DEG:
298                                 case FORMAT_DEG_WITHOUT_CARDINAL:
299                                 {
300                                         answer = (_asDouble<0.0?"-":"")
301                                                 + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
302                                         break;
303                                 }
304                                 case FORMAT_DECIMAL_FORCE_POINT:
305                                 {
306                                         // Forcing a decimal point instead of system-dependent commas etc
307                                         answer = EIGHT_DP.format(_asDouble);
308                                         break;
309                                 }
310                                 case FORMAT_DEG_MIN_SEC_WITH_SPACES:
311                                 {
312                                         // Note: cardinal not needed as this format is only for exif, which has cardinal separately
313                                         answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
314                                         break;
315                                 }
316                                 case FORMAT_CARDINAL:
317                                 {
318                                         answer = "" + PRINTABLE_CARDINALS[_cardinal];
319                                         break;
320                                 }
321                         }
322                 }
323                 return answer;
324         }
325
326         /**
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
331          */
332         private static final String formatFraction(int inFrac, int inDenom)
333         {
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);
340         }
341
342
343         /**
344          * Format an integer to a two-digit String
345          * @param inNumber number to format
346          * @return two-character String
347          */
348         private static String twoDigitString(int inNumber)
349         {
350                 if (inNumber <= 0) return "00";
351                 if (inNumber < 10) return "0" + inNumber;
352                 if (inNumber < 100) return "" + inNumber;
353                 return "" + (inNumber % 100);
354         }
355
356
357         /**
358          * Format an integer to a three-digit String for degrees
359          * @param inNumber number to format
360          * @return three-character String
361          */
362         private static String threeDigitString(int inNumber)
363         {
364                 if (inNumber <= 0) return "000";
365                 if (inNumber < 10) return "00" + inNumber;
366                 if (inNumber < 100) return "0" + inNumber;
367                 return "" + (inNumber % 1000);
368         }
369
370
371         /**
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
378          */
379         public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
380                 int inIndex, int inNumPoints)
381         {
382                 return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
383         }
384
385
386         /**
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
392          */
393         public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
394                 double inFraction)
395         {
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);
400                 return answer;
401         }
402
403
404         /**
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
409          */
410         protected abstract Coordinate makeNew(double inValue, int inFormat);
411
412
413         /**
414          * Create a String representation for debug
415          * @return String describing coordinate value
416          */
417         public String toString()
418         {
419                 return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
420                         + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;
421         }
422 }