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