]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/data/Coordinate.java
Version 4, January 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                         _cardinal = getCardinal(inString.charAt(0), inString.charAt(strLen-1));
54                         // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
55                         int numFields = 0;
56                         boolean inNumeric = false;
57                         char currChar;
58                         long[] fields = new long[4]; // needs to be long for lengthy decimals
59                         long[] denoms = new long[4];
60                         String secondDelim = "";
61                         try
62                         {
63                                 // Loop over characters in input string, populating fields array
64                                 for (int i=0; i<strLen; i++)
65                                 {
66                                         currChar = inString.charAt(i);
67                                         if (currChar >= '0' && currChar <= '9')
68                                         {
69                                                 if (!inNumeric)
70                                                 {
71                                                         inNumeric = true;
72                                                         numFields++;
73                                                         denoms[numFields-1] = 1;
74                                                 }
75                                                 fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0');
76                                                 denoms[numFields-1] *= 10;
77                                         }
78                                         else
79                                         {
80                                                 inNumeric = false;
81                                                 // Remember second delimiter
82                                                 if (numFields == 2) {
83                                                         secondDelim += currChar;
84                                                 }
85                                         }
86                                 }
87                                 _valid = (numFields > 0);
88                         }
89                         catch (ArrayIndexOutOfBoundsException obe)
90                         {
91                                 // more than four fields found - unable to parse
92                                 _valid = false;
93                         }
94                         // parse fields according to number found
95                         _degrees = (int) fields[0];
96                         _originalFormat = FORMAT_DEG;
97                         _fracDenom = 10;
98                         if (numFields == 2)
99                         {
100                                 // String is just decimal degrees
101                                 double numMins = fields[1] * 60.0 / denoms[1];
102                                 _minutes = (int) numMins;
103                                 double numSecs = (numMins - _minutes) * 60.0;
104                                 _seconds = (int) numSecs;
105                                 _fracs = (int) ((numSecs - _seconds) * 10);
106                         }
107                         // Differentiate between d-m.f and d-m-s using . or ,
108                         else if (numFields == 3 && (secondDelim.equals(".") || secondDelim.equals(",")))
109                         {
110                                 // String is degrees-minutes.fractions
111                                 _originalFormat = FORMAT_DEG_MIN;
112                                 _minutes = (int) fields[1];
113                                 double numSecs = fields[2] * 60.0 / denoms[2];
114                                 _seconds = (int) numSecs;
115                                 _fracs = (int) ((numSecs - _seconds) * 10);
116                         }
117                         else if (numFields == 4 || numFields == 3)
118                         {
119                                 // String is degrees-minutes-seconds.fractions
120                                 _originalFormat = FORMAT_DEG_MIN_SEC;
121                                 _minutes = (int) fields[1];
122                                 _seconds = (int) fields[2];
123                                 _fracs = (int) fields[3];
124                                 _fracDenom = (int) denoms[3];
125                                 if (_fracDenom < 1) {_fracDenom = 1;}
126                         }
127                         _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
128                         if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
129                                 _asDouble = -_asDouble;
130                         // validate fields
131                         _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
132                 }
133                 else _valid = false;
134         }
135
136
137         /**
138          * Get the cardinal from the given character
139          * @param inFirstChar first character from file
140          * @param inLastChar last character from file
141          */
142         protected int getCardinal(char inFirstChar, char inLastChar)
143         {
144                 // Try leading character first
145                 int cardinal = getCardinal(inFirstChar);
146                 // if not there, try trailing character
147                 if (cardinal == NO_CARDINAL) {
148                         cardinal = getCardinal(inLastChar);
149                 }
150                 // use default from concrete subclass
151                 if (cardinal == NO_CARDINAL) {
152                         cardinal = getDefaultCardinal();
153                 }
154                 return cardinal;
155         }
156
157
158         /**
159          * Get the cardinal from the given character
160          * @param inChar character from file
161          */
162         protected abstract int getCardinal(char inChar);
163
164         /**
165          * @return the default cardinal for the subclass
166          */
167         protected abstract int getDefaultCardinal();
168
169         /**
170          * @return the maximum degree range for this coordinate
171          */
172         protected abstract int getMaxDegrees();
173
174
175         /**
176          * Constructor
177          * @param inValue value of coordinate
178          * @param inFormat format to use
179          * @param inCardinal cardinal
180          */
181         protected Coordinate(double inValue, int inFormat, int inCardinal)
182         {
183                 _asDouble = inValue;
184                 // Calculate degrees, minutes, seconds
185                 _degrees = (int) Math.abs(inValue);
186                 double numMins = (Math.abs(_asDouble)-_degrees) * 60.0;
187                 _minutes = (int) numMins;
188                 double numSecs = (numMins - _minutes) * 60.0;
189                 _seconds = (int) numSecs;
190                 _fracs = (int) ((numSecs - _seconds) * 10);
191                 _fracDenom = 10; // fixed for now
192                 // Make a string to display on screen
193                 _cardinal = inCardinal;
194                 _originalFormat = FORMAT_NONE;
195                 if (inFormat == FORMAT_NONE) inFormat = FORMAT_DEG_WITHOUT_CARDINAL;
196                 _originalString = output(inFormat);
197                 _originalFormat = inFormat;
198                 _valid = true;
199         }
200
201
202         /**
203          * @return coordinate as a double
204          */
205         public double getDouble()
206         {
207                 return _asDouble;
208         }
209
210         /**
211          * @return true if Coordinate is valid
212          */
213         public boolean isValid()
214         {
215                 return _valid;
216         }
217
218         /**
219          * Compares two Coordinates for equality
220          * @param inOther other Coordinate object with which to compare
221          * @return true if the two objects are equal
222          */
223         public boolean equals(Coordinate inOther)
224         {
225                 return (inOther != null && _cardinal == inOther._cardinal
226                         && _degrees == inOther._degrees
227                         && _minutes == inOther._minutes
228                         && _seconds == inOther._seconds
229                         && _fracs == inOther._fracs);
230         }
231
232
233         /**
234          * Output the Coordinate in the given format
235          * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
236          * @return String for output
237          */
238         public String output(int inFormat)
239         {
240                 String answer = _originalString;
241                 if (inFormat != FORMAT_NONE && inFormat != _originalFormat)
242                 {
243                         // TODO: allow specification of precision for output of d-m and d
244                         // format as specified
245                         switch (inFormat)
246                         {
247                                 case FORMAT_DEG_MIN_SEC:
248                                 {
249                                         StringBuffer buffer = new StringBuffer();
250                                         buffer.append(PRINTABLE_CARDINALS[_cardinal])
251                                                 .append(threeDigitString(_degrees)).append('°')
252                                                 .append(twoDigitString(_minutes)).append('\'')
253                                                 .append(twoDigitString(_seconds)).append('.')
254                                                 .append(formatFraction(_fracs, _fracDenom));
255                                         answer = buffer.toString();
256                                         break;
257                                 }
258                                 case FORMAT_DEG_MIN:
259                                 {
260                                         answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
261                                                 + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
262                                         break;
263                                 }
264                                 case FORMAT_DEG_WHOLE_MIN:
265                                 {
266                                         answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
267                                                 + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5) + "'";
268                                         break;
269                                 }
270                                 case FORMAT_DEG:
271                                 case FORMAT_DEG_WITHOUT_CARDINAL:
272                                 {
273                                         answer = (_asDouble<0.0?"-":"")
274                                                 + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
275                                         break;
276                                 }
277                                 case FORMAT_DEG_MIN_SEC_WITH_SPACES:
278                                 {
279                                         // Note: cardinal not needed as this format is only for exif, which has cardinal separately
280                                         answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
281                                         break;
282                                 }
283                                 case FORMAT_CARDINAL:
284                                 {
285                                         answer = "" + PRINTABLE_CARDINALS[_cardinal];
286                                         break;
287                                 }
288                         }
289                 }
290                 return answer;
291         }
292
293         /**
294          * Format the fraction part of seconds value
295          * @param inFrac fractional part eg 123
296          * @param inDenom denominator of fraction eg 10000
297          * @return String describing fraction, in this case 0123
298          */
299         private static final String formatFraction(int inFrac, int inDenom)
300         {
301                 if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
302                 String denomString = "" + inDenom;
303                 int reqdLen = denomString.length() - 1;
304                 String result = denomString + inFrac;
305                 int resultLen = result.length();
306                 return result.substring(resultLen - reqdLen);
307         }
308
309
310         /**
311          * Format an integer to a two-digit String
312          * @param inNumber number to format
313          * @return two-character String
314          */
315         private static String twoDigitString(int inNumber)
316         {
317                 if (inNumber <= 0) return "00";
318                 if (inNumber < 10) return "0" + inNumber;
319                 if (inNumber < 100) return "" + inNumber;
320                 return "" + (inNumber % 100);
321         }
322
323
324         /**
325          * Format an integer to a three-digit String for degrees
326          * @param inNumber number to format
327          * @return three-character String
328          */
329         private static String threeDigitString(int inNumber)
330         {
331                 if (inNumber <= 0) return "000";
332                 if (inNumber < 10) return "00" + inNumber;
333                 if (inNumber < 100) return "0" + inNumber;
334                 return "" + (inNumber % 1000);
335         }
336
337
338         /**
339          * Create a new Coordinate between two others
340          * @param inStart start coordinate
341          * @param inEnd end coordinate
342          * @param inIndex index of point
343          * @param inNumPoints number of points to interpolate
344          * @return new Coordinate object
345          */
346         public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
347                 int inIndex, int inNumPoints)
348         {
349                 return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
350         }
351
352
353         /**
354          * Create a new Coordinate between two others
355          * @param inStart start coordinate
356          * @param inEnd end coordinate
357          * @param inFraction fraction from start to end
358          * @return new Coordinate object
359          */
360         public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
361                 double inFraction)
362         {
363                 double startValue = inStart.getDouble();
364                 double endValue = inEnd.getDouble();
365                 double newValue = startValue + (endValue - startValue) * inFraction;
366                 Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat);
367                 return answer;
368         }
369
370
371         /**
372          * Make a new Coordinate according to subclass
373          * @param inValue double value
374          * @param inFormat format to use
375          * @return object of Coordinate subclass
376          */
377         protected abstract Coordinate makeNew(double inValue, int inFormat);
378
379
380         /**
381          * Create a String representation for debug
382          * @return String describing coordinate value
383          */
384         public String toString()
385         {
386                 return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
387                         + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;
388         }
389 }