]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/data/Coordinate.java
Version 6, October 2008
[GpsPrune.git] / tim / prune / data / Coordinate.java
1 package tim.prune.data;
2
3 /**
4  * Class to represent a lat/long coordinate
5  * and provide conversion functions
6  */
7 public abstract class Coordinate
8 {
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;
23
24         // Instance variables
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;
35
36
37         /**
38          * Constructor given String
39          * @param inString string to parse
40          */
41         public Coordinate(String inString)
42         {
43                 _originalString = inString;
44                 int strLen = 0;
45                 if (inString != null)
46                 {
47                         inString = inString.trim();
48                         strLen = inString.length();
49                 }
50                 if (strLen > 1)
51                 {
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) {
56                                 hasCardinal = false;
57                                 // use default from concrete subclass
58                                 _cardinal = getDefaultCardinal();
59                         }
60
61                         // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
62                         int numFields = 0;
63                         boolean inNumeric = false;
64                         char currChar;
65                         long[] fields = new long[4]; // needs to be long for lengthy decimals
66                         long[] denoms = new long[4];
67                         String secondDelim = "";
68                         try
69                         {
70                                 // Loop over characters in input string, populating fields array
71                                 for (int i=0; i<strLen; i++)
72                                 {
73                                         currChar = inString.charAt(i);
74                                         if (currChar >= '0' && currChar <= '9')
75                                         {
76                                                 if (!inNumeric)
77                                                 {
78                                                         inNumeric = true;
79                                                         numFields++;
80                                                         denoms[numFields-1] = 1;
81                                                 }
82                                                 fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0');
83                                                 denoms[numFields-1] *= 10;
84                                         }
85                                         else
86                                         {
87                                                 inNumeric = false;
88                                                 // Remember second delimiter
89                                                 if (numFields == 2) {
90                                                         secondDelim += currChar;
91                                                 }
92                                         }
93                                 }
94                                 _valid = (numFields > 0);
95                         }
96                         catch (ArrayIndexOutOfBoundsException obe)
97                         {
98                                 // more than four fields found - unable to parse
99                                 _valid = false;
100                         }
101                         // parse fields according to number found
102                         _degrees = (int) fields[0];
103                         _originalFormat = hasCardinal?FORMAT_DEG:FORMAT_DEG_WITHOUT_CARDINAL;
104                         _fracDenom = 10;
105                         if (numFields == 2)
106                         {
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);
113                         }
114                         // Differentiate between d-m.f and d-m-s using . or ,
115                         else if (numFields == 3 && (secondDelim.equals(".") || secondDelim.equals(",")))
116                         {
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);
123                         }
124                         else if (numFields == 4 || numFields == 3)
125                         {
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;}
133                         }
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;
137                         // validate fields
138                         _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
139                 }
140                 else _valid = false;
141         }
142
143
144         /**
145          * Get the cardinal from the given character
146          * @param inFirstChar first character from file
147          * @param inLastChar last character from file
148          */
149         protected int getCardinal(char inFirstChar, char inLastChar)
150         {
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);
156                 }
157                 return cardinal;
158         }
159
160
161         /**
162          * Get the cardinal from the given character
163          * @param inChar character from file
164          */
165         protected abstract int getCardinal(char inChar);
166
167         /**
168          * @return the default cardinal for the subclass
169          */
170         protected abstract int getDefaultCardinal();
171
172         /**
173          * @return the maximum degree range for this coordinate
174          */
175         protected abstract int getMaxDegrees();
176
177
178         /**
179          * Constructor
180          * @param inValue value of coordinate
181          * @param inFormat format to use
182          * @param inCardinal cardinal
183          */
184         protected Coordinate(double inValue, int inFormat, int inCardinal)
185         {
186                 _asDouble = inValue;
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;
201                 _valid = true;
202         }
203
204
205         /**
206          * @return coordinate as a double
207          */
208         public double getDouble()
209         {
210                 return _asDouble;
211         }
212
213         /**
214          * @return true if Coordinate is valid
215          */
216         public boolean isValid()
217         {
218                 return _valid;
219         }
220
221         /**
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
225          */
226         public boolean equals(Coordinate inOther)
227         {
228                 return (inOther != null && _cardinal == inOther._cardinal
229                         && _degrees == inOther._degrees
230                         && _minutes == inOther._minutes
231                         && _seconds == inOther._seconds
232                         && _fracs == inOther._fracs);
233         }
234
235
236         /**
237          * Output the Coordinate in the given format
238          * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
239          * @return String for output
240          */
241         public String output(int inFormat)
242         {
243                 String answer = _originalString;
244                 if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
245                 {
246                         // TODO: allow specification of precision for output of d-m and d
247                         // format as specified
248                         switch (inFormat)
249                         {
250                                 case FORMAT_DEG_MIN_SEC:
251                                 {
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();
259                                         break;
260                                 }
261                                 case FORMAT_DEG_MIN:
262                                 {
263                                         answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
264                                                 + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
265                                         break;
266                                 }
267                                 case FORMAT_DEG_WHOLE_MIN:
268                                 {
269                                         answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
270                                                 + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5) + "'";
271                                         break;
272                                 }
273                                 case FORMAT_DEG:
274                                 case FORMAT_DEG_WITHOUT_CARDINAL:
275                                 {
276                                         answer = (_asDouble<0.0?"-":"")
277                                                 + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
278                                         break;
279                                 }
280                                 case FORMAT_DEG_MIN_SEC_WITH_SPACES:
281                                 {
282                                         // Note: cardinal not needed as this format is only for exif, which has cardinal separately
283                                         answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
284                                         break;
285                                 }
286                                 case FORMAT_CARDINAL:
287                                 {
288                                         answer = "" + PRINTABLE_CARDINALS[_cardinal];
289                                         break;
290                                 }
291                         }
292                 }
293                 return answer;
294         }
295
296         /**
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
301          */
302         private static final String formatFraction(int inFrac, int inDenom)
303         {
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);
310         }
311
312
313         /**
314          * Format an integer to a two-digit String
315          * @param inNumber number to format
316          * @return two-character String
317          */
318         private static String twoDigitString(int inNumber)
319         {
320                 if (inNumber <= 0) return "00";
321                 if (inNumber < 10) return "0" + inNumber;
322                 if (inNumber < 100) return "" + inNumber;
323                 return "" + (inNumber % 100);
324         }
325
326
327         /**
328          * Format an integer to a three-digit String for degrees
329          * @param inNumber number to format
330          * @return three-character String
331          */
332         private static String threeDigitString(int inNumber)
333         {
334                 if (inNumber <= 0) return "000";
335                 if (inNumber < 10) return "00" + inNumber;
336                 if (inNumber < 100) return "0" + inNumber;
337                 return "" + (inNumber % 1000);
338         }
339
340
341         /**
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
348          */
349         public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
350                 int inIndex, int inNumPoints)
351         {
352                 return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
353         }
354
355
356         /**
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
362          */
363         public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
364                 double inFraction)
365         {
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);
370                 return answer;
371         }
372
373
374         /**
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
379          */
380         protected abstract Coordinate makeNew(double inValue, int inFormat);
381
382
383         /**
384          * Create a String representation for debug
385          * @return String describing coordinate value
386          */
387         public String toString()
388         {
389                 return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
390                         + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;
391         }
392 }