GeographicLib  2.6
DMS.cpp
Go to the documentation of this file.
1 /**
2  * \file DMS.cpp
3  * \brief Implementation for GeographicLib::DMS class
4  *
5  * Copyright (c) Charles Karney (2008-2022) <karney@alum.mit.edu> and licensed
6  * under the MIT/X11 License. For more information, see
7  * https://geographiclib.sourceforge.io/
8  **********************************************************************/
9 
10 #include <GeographicLib/DMS.hpp>
12 
13 #if defined(_MSC_VER)
14 // Squelch warnings about unrepresentable characters
15 # pragma warning (disable: 4819)
16 #endif
17 
18 namespace GeographicLib {
19 
20  using namespace std;
21 
22  const char* const DMS::hemispheres_ = "SNWE";
23  const char* const DMS::signs_ = "-+";
24  const char* const DMS::digits_ = "0123456789";
25  const char* const DMS::dmsindicators_ = "D'\":";
26  const char* const DMS::components_[] = {"degrees", "minutes", "seconds"};
27 
28  // Replace all occurrences of pat by c. If c is NULL remove pat.
29  void DMS::replace(std::string& s, const std::string& pat, char c) {
30  string::size_type p = 0;
31  int count = c ? 1 : 0;
32  while (true) {
33  p = s.find(pat, p);
34  if (p == string::npos)
35  break;
36  s.replace(p, pat.length(), count, c);
37  }
38  }
39 
40  Math::real DMS::Decode(const std::string& dms, flag& ind) {
41  // Here's a table of the allowed characters
42 
43  // S unicode dec UTF-8 descripton
44 
45  // DEGREE
46  // d U+0064 100 64 d
47  // D U+0044 68 44 D
48  // ° U+00b0 176 c2 b0 degree symbol
49  // º U+00ba 186 c2 ba alt symbol
50  // ⁰ U+2070 8304 e2 81 b0 sup zero
51  // ˚ U+02da 730 cb 9a ring above
52  // ∘ U+2218 8728 e2 88 98 compose function
53  // * U+002a 42 2a GRiD symbol for degrees
54 
55  // MINUTES
56  // ' U+0027 39 27 apostrophe
57  // ` U+0060 96 60 grave accent
58  // ′ U+2032 8242 e2 80 b2 prime
59  // ‵ U+2035 8245 e2 80 b5 back prime
60  // ´ U+00b4 180 c2 b4 acute accent
61  // ‘ U+2018 8216 e2 80 98 left single quote (also ext ASCII 0x91)
62  // ’ U+2019 8217 e2 80 99 right single quote (also ext ASCII 0x92)
63  // ‛ U+201b 8219 e2 80 9b reversed-9 single quote
64  // ʹ U+02b9 697 ca b9 modifier letter prime
65  // ˊ U+02ca 714 cb 8a modifier letter acute accent
66  // ˋ U+02cb 715 cb 8b modifier letter grave accent
67 
68  // SECONDS
69  // " U+0022 34 22 quotation mark
70  // ″ U+2033 8243 e2 80 b3 double prime
71  // ‶ U+2036 8246 e2 80 b6 reversed double prime
72  // ˝ U+02dd 733 cb 9d double acute accent
73  // “ U+201c 8220 e2 80 9c left double quote (also ext ASCII 0x93)
74  // ” U+201d 8221 e2 80 9d right double quote (also ext ASCII 0x94)
75  // ‟ U+201f 8223 e2 80 9f reversed-9 double quote
76  // ʺ U+02ba 698 ca ba modifier letter double prime
77 
78  // PLUS
79  // + U+002b 43 2b plus sign
80  // ➕ U+2795 10133 e2 9e 95 heavy plus
81  // U+2064 8292 e2 81 a4 invisible plus |⁤|
82 
83  // MINUS
84  // - U+002d 45 2d hyphen
85  // ‐ U+2010 8208 e2 80 90 dash
86  // ‑ U+2011 8209 e2 80 91 non-breaking hyphen
87  // – U+2013 8211 e2 80 93 en dash (also ext ASCII 0x96)
88  // — U+2014 8212 e2 80 94 em dash (also ext ASCII 0x97)
89  // − U+2212 8722 e2 88 92 minus sign
90  // ➖ U+2796 10134 e2 9e 96 heavy minus
91 
92  // IGNORED
93  //   U+00a0 160 c2 a0 non-breaking space
94  // U+2007 8199 e2 80 87 figure space | |
95  // U+2009 8201 e2 80 89 thin space | |
96  // U+200a 8202 e2 80 8a hair space | |
97  // U+200b 8203 e2 80 8b invisible space |​|
98  //   U+202f 8239 e2 80 af narrow space | |
99  // U+2063 8291 e2 81 a3 invisible separator |⁣|
100  // « U+00ab 171 c2 ab left guillemot (for cgi-bin)
101  // » U+00bb 187 c2 bb right guillemot (for cgi-bin)
102 
103  string dmsa = dms;
104  replace(dmsa, "\xc2\xb0", 'd' ); // U+00b0 degree symbol
105  replace(dmsa, "\xc2\xba", 'd' ); // U+00ba alt symbol
106  replace(dmsa, "\xe2\x81\xb0", 'd' ); // U+2070 sup zero
107  replace(dmsa, "\xcb\x9a", 'd' ); // U+02da ring above
108  replace(dmsa, "\xe2\x88\x98", 'd' ); // U+2218 compose function
109 
110  replace(dmsa, "\xe2\x80\xb2", '\''); // U+2032 prime
111  replace(dmsa, "\xe2\x80\xb5", '\''); // U+2035 back prime
112  replace(dmsa, "\xc2\xb4", '\''); // U+00b4 acute accent
113  replace(dmsa, "\xe2\x80\x98", '\''); // U+2018 left single quote
114  replace(dmsa, "\xe2\x80\x99", '\''); // U+2019 right single quote
115  replace(dmsa, "\xe2\x80\x9b", '\''); // U+201b reversed-9 single quote
116  replace(dmsa, "\xca\xb9", '\''); // U+02b9 modifier letter prime
117  replace(dmsa, "\xcb\x8a", '\''); // U+02ca modifier letter acute accent
118  replace(dmsa, "\xcb\x8b", '\''); // U+02cb modifier letter grave accent
119 
120  replace(dmsa, "\xe2\x80\xb3", '"' ); // U+2033 double prime
121  replace(dmsa, "\xe2\x80\xb6", '"' ); // U+2036 reversed double prime
122  replace(dmsa, "\xcb\x9d", '"' ); // U+02dd double acute accent
123  replace(dmsa, "\xe2\x80\x9c", '"' ); // U+201c left double quote
124  replace(dmsa, "\xe2\x80\x9d", '"' ); // U+201d right double quote
125  replace(dmsa, "\xe2\x80\x9f", '"' ); // U+201f reversed-9 double quote
126  replace(dmsa, "\xca\xba", '"' ); // U+02ba modifier letter double prime
127 
128  replace(dmsa, "\xe2\x9e\x95", '+' ); // U+2795 heavy plus
129  replace(dmsa, "\xe2\x81\xa4", '+' ); // U+2064 invisible plus
130 
131  replace(dmsa, "\xe2\x80\x90", '-' ); // U+2010 dash
132  replace(dmsa, "\xe2\x80\x91", '-' ); // U+2011 non-breaking hyphen
133  replace(dmsa, "\xe2\x80\x93", '-' ); // U+2013 en dash
134  replace(dmsa, "\xe2\x80\x94", '-' ); // U+2014 em dash
135  replace(dmsa, "\xe2\x88\x92", '-' ); // U+2212 minus sign
136  replace(dmsa, "\xe2\x9e\x96", '-' ); // U+2796 heavy minus
137 
138  replace(dmsa, "\xc2\xa0", '\0'); // U+00a0 non-breaking space
139  replace(dmsa, "\xe2\x80\x87", '\0'); // U+2007 figure space
140  replace(dmsa, "\xe2\x80\x89", '\0'); // U+2007 thin space
141  replace(dmsa, "\xe2\x80\x8a", '\0'); // U+200a hair space
142  replace(dmsa, "\xe2\x80\x8b", '\0'); // U+200b invisible space
143  replace(dmsa, "\xe2\x80\xaf", '\0'); // U+202f narrow space
144  replace(dmsa, "\xe2\x81\xa3", '\0'); // U+2063 invisible separator
145 
146  replace(dmsa, "\xb0", 'd' ); // 0xb0 bare degree symbol
147  replace(dmsa, "\xba", 'd' ); // 0xba bare alt symbol
148  replace(dmsa, "*", 'd' ); // GRiD symbol for degree
149  replace(dmsa, "`", '\''); // grave accent
150  replace(dmsa, "\xb4", '\''); // 0xb4 bare acute accent
151  // Don't implement these alternatives; they are only relevant for cgi-bin
152  // replace(dmsa, "\x91", '\''); // 0x91 ext ASCII left single quote
153  // replace(dmsa, "\x92", '\''); // 0x92 ext ASCII right single quote
154  // replace(dmsa, "\x93", '"' ); // 0x93 ext ASCII left double quote
155  // replace(dmsa, "\x94", '"' ); // 0x94 ext ASCII right double quote
156  // replace(dmsa, "\x96", '-' ); // 0x96 ext ASCII en dash
157  // replace(dmsa, "\x97", '-' ); // 0x97 ext ASCII em dash
158  replace(dmsa, "\xa0", '\0'); // 0xa0 bare non-breaking space
159  replace(dmsa, "''", '"' ); // '' -> "
160  string::size_type
161  beg = 0,
162  end = unsigned(dmsa.size());
163  while (beg < end && isspace(dmsa[beg]))
164  ++beg;
165  while (beg < end && isspace(dmsa[end - 1]))
166  --end;
167  // The trimmed string in [beg, end)
168  real v = -0.0; // So "-0" returns -0.0
169  int i = 0;
170  flag ind1 = NONE;
171  // p is pointer to the next piece that needs decoding
172  for (string::size_type p = beg, pb; p < end; p = pb, ++i) {
173  string::size_type pa = p;
174  // Skip over initial hemisphere letter (for i == 0)
175  if (i == 0 && Utility::lookup(hemispheres_, dmsa[pa]) >= 0)
176  ++pa;
177  // Skip over initial sign (checking for it if i == 0)
178  if (i > 0 || (pa < end && Utility::lookup(signs_, dmsa[pa]) >= 0))
179  ++pa;
180  // Find next sign
181  pb = min(dmsa.find_first_of(signs_, pa), end);
182  flag ind2 = NONE;
183  v += InternalDecode(dmsa.substr(p, pb - p), ind2);
184  if (ind1 == NONE)
185  ind1 = ind2;
186  else if (!(ind2 == NONE || ind1 == ind2))
187  throw GeographicErr("Incompatible hemisphere specifier in " +
188  dmsa.substr(beg, pb - beg));
189  }
190  if (i == 0)
191  throw GeographicErr("Empty or incomplete DMS string " +
192  dmsa.substr(beg, end - beg));
193  ind = ind1;
194  return v;
195  }
196 
197  Math::real DMS::InternalDecode(const string& dmsa, flag& ind) {
198  const int maxcomponents = 3;
199  string errormsg;
200  do { // Executed once (provides the ability to break)
201  int sign = 1;
202  unsigned
203  beg = 0,
204  end = unsigned(dmsa.size());
205  flag ind1 = NONE;
206  int k = -1;
207  if (end > beg && (k = Utility::lookup(hemispheres_, dmsa[beg])) >= 0) {
208  ind1 = (k / 2) ? LONGITUDE : LATITUDE;
209  sign = k % 2 ? 1 : -1;
210  ++beg;
211  }
212  if (end > beg && (k = Utility::lookup(hemispheres_, dmsa[end-1])) >= 0) {
213  if (k >= 0) {
214  if (ind1 != NONE) {
215  if (toupper(dmsa[beg - 1]) == toupper(dmsa[end - 1]))
216  errormsg = "Repeated hemisphere indicators "
217  + Utility::str(dmsa[beg - 1])
218  + " in " + dmsa.substr(beg - 1, end - beg + 1);
219  else
220  errormsg = "Contradictory hemisphere indicators "
221  + Utility::str(dmsa[beg - 1]) + " and "
222  + Utility::str(dmsa[end - 1]) + " in "
223  + dmsa.substr(beg - 1, end - beg + 1);
224  break;
225  }
226  ind1 = (k / 2) ? LONGITUDE : LATITUDE;
227  sign = k % 2 ? 1 : -1;
228  --end;
229  }
230  }
231  if (end > beg && (k = Utility::lookup(signs_, dmsa[beg])) >= 0) {
232  if (k >= 0) {
233  sign *= k ? 1 : -1;
234  ++beg;
235  }
236  }
237  if (end == beg) {
238  errormsg = "Empty or incomplete DMS string " + dmsa;
239  break;
240  }
241  real ipieces[maxcomponents] = {0, 0, 0};
242  real fpieces[maxcomponents] = {0, 0, 0};
243  unsigned npiece = 0;
244  real icurrent = 0;
245  real fcurrent = 0;
246  unsigned ncurrent = 0, p = beg;
247  bool pointseen = false;
248  unsigned digcount = 0, intcount = 0;
249  while (p < end) {
250  char x = dmsa[p++];
251  if ((k = Utility::lookup(digits_, x)) >= 0) {
252  ++ncurrent;
253  if (digcount > 0)
254  ++digcount; // Count of decimal digits
255  else {
256  icurrent = 10 * icurrent + k;
257  ++intcount;
258  }
259  } else if (x == '.') {
260  if (pointseen) {
261  errormsg = "Multiple decimal points in "
262  + dmsa.substr(beg, end - beg);
263  break;
264  }
265  pointseen = true;
266  digcount = 1;
267  } else if ((k = Utility::lookup(dmsindicators_, x)) >= 0) {
268  if (k >= maxcomponents) {
269  if (p == end) {
270  errormsg = "Illegal for : to appear at the end of " +
271  dmsa.substr(beg, end - beg);
272  break;
273  }
274  k = npiece;
275  }
276  if (unsigned(k) == npiece - 1) {
277  errormsg = "Repeated " + string(components_[k]) +
278  " component in " + dmsa.substr(beg, end - beg);
279  break;
280  } else if (unsigned(k) < npiece) {
281  errormsg = string(components_[k]) + " component follows "
282  + string(components_[npiece - 1]) + " component in "
283  + dmsa.substr(beg, end - beg);
284  break;
285  }
286  if (ncurrent == 0) {
287  errormsg = "Missing numbers in " + string(components_[k]) +
288  " component of " + dmsa.substr(beg, end - beg);
289  break;
290  }
291  if (digcount > 0) {
292  istringstream s(dmsa.substr(p - intcount - digcount - 1,
293  intcount + digcount));
294  s >> fcurrent;
295  icurrent = 0;
296  }
297  ipieces[k] = icurrent;
298  fpieces[k] = icurrent + fcurrent;
299  if (p < end) {
300  npiece = k + 1;
301  if (npiece >= maxcomponents) {
302  errormsg = "More than 3 DMS components in "
303  + dmsa.substr(beg, end - beg);
304  break;
305  }
306  icurrent = fcurrent = 0;
307  ncurrent = digcount = intcount = 0;
308  }
309  } else if (Utility::lookup(signs_, x) >= 0) {
310  errormsg = "Internal sign in DMS string "
311  + dmsa.substr(beg, end - beg);
312  break;
313  } else {
314  errormsg = "Illegal character " + Utility::str(x) + " in DMS string "
315  + dmsa.substr(beg, end - beg);
316  break;
317  }
318  }
319  if (!errormsg.empty())
320  break;
321  if (Utility::lookup(dmsindicators_, dmsa[p - 1]) < 0) {
322  if (npiece >= maxcomponents) {
323  errormsg = "Extra text following seconds in DMS string "
324  + dmsa.substr(beg, end - beg);
325  break;
326  }
327  if (ncurrent == 0) {
328  errormsg = "Missing numbers in trailing component of "
329  + dmsa.substr(beg, end - beg);
330  break;
331  }
332  if (digcount > 0) {
333  istringstream s(dmsa.substr(p - intcount - digcount,
334  intcount + digcount));
335  s >> fcurrent;
336  icurrent = 0;
337  }
338  ipieces[npiece] = icurrent;
339  fpieces[npiece] = icurrent + fcurrent;
340  }
341  if (pointseen && digcount == 0) {
342  errormsg = "Decimal point in non-terminal component of "
343  + dmsa.substr(beg, end - beg);
344  break;
345  }
346  // Note that we accept 59.999999... even though it rounds to 60.
347  if (ipieces[1] >= Math::dm || fpieces[1] > Math::dm ) {
348  errormsg = "Minutes " + Utility::str(fpieces[1])
349  + " not in range [0, " + to_string(Math::dm) + ")";
350  break;
351  }
352  if (ipieces[2] >= Math::ms || fpieces[2] > Math::ms) {
353  errormsg = "Seconds " + Utility::str(fpieces[2])
354  + " not in range [0, " + to_string(Math::ms) + ")";
355  break;
356  }
357  ind = ind1;
358  // Assume check on range of result is made by calling routine (which
359  // might be able to offer a better diagnostic).
360  return real(sign) *
361  ( fpieces[2] != 0 ?
362  (Math::ms*(Math::dm*fpieces[0] + fpieces[1]) + fpieces[2])/Math::ds :
363  ( fpieces[1] != 0 ?
364  (Math::dm*fpieces[0] + fpieces[1]) / Math::dm : fpieces[0] ) );
365  } while (false);
366  real val = Utility::nummatch<real>(dmsa);
367  if (val == 0)
368  throw GeographicErr(errormsg);
369  else
370  ind = NONE;
371  return val;
372  }
373 
374  void DMS::DecodeLatLon(const string& stra, const string& strb,
375  real& lat, real& lon,
376  bool longfirst) {
377  real a, b;
378  flag ia, ib;
379  a = Decode(stra, ia);
380  b = Decode(strb, ib);
381  if (ia == NONE && ib == NONE) {
382  // Default to lat, long unless longfirst
383  ia = longfirst ? LONGITUDE : LATITUDE;
384  ib = longfirst ? LATITUDE : LONGITUDE;
385  } else if (ia == NONE)
386  ia = flag(LATITUDE + LONGITUDE - ib);
387  else if (ib == NONE)
388  ib = flag(LATITUDE + LONGITUDE - ia);
389  if (ia == ib)
390  throw GeographicErr("Both " + stra + " and "
391  + strb + " interpreted as "
392  + (ia == LATITUDE ? "latitudes" : "longitudes"));
393  real
394  lat1 = ia == LATITUDE ? a : b,
395  lon1 = ia == LATITUDE ? b : a;
396  if (fabs(lat1) > Math::qd)
397  throw GeographicErr("Latitude " + Utility::str(lat1)
398  + "d not in [-" + to_string(Math::qd)
399  + "d, " + to_string(Math::qd) + "d]");
400  lat = lat1;
401  lon = lon1;
402  }
403 
404  Math::real DMS::DecodeAngle(const string& angstr) {
405  flag ind;
406  real ang = Decode(angstr, ind);
407  if (ind != NONE)
408  throw GeographicErr("Arc angle " + angstr
409  + " includes a hemisphere, N/E/W/S");
410  return ang;
411  }
412 
413  Math::real DMS::DecodeAzimuth(const string& azistr) {
414  flag ind;
415  real azi = Decode(azistr, ind);
416  if (ind == LATITUDE)
417  throw GeographicErr("Azimuth " + azistr
418  + " has a latitude hemisphere, N/S");
419  return Math::AngNormalize(azi);
420  }
421 
422  string DMS::Encode(real angle, component trailing, unsigned prec, flag ind,
423  char dmssep) {
424  // Assume check on range of input angle has been made by calling
425  // routine (which might be able to offer a better diagnostic).
426  if (!isfinite(angle))
427  return angle < 0 ? string("-inf") :
428  (angle > 0 ? string("inf") : string("nan"));
429 
430  // 15 - 2 * trailing = ceiling(log10(2^53/90/60^trailing)).
431  // This suffices to give full real precision for numbers in [-90,90]
432  prec = min(15 + Math::extra_digits() - 2 * unsigned(trailing), prec);
433  real scale = trailing == MINUTE ? Math::dm :
434  (trailing == SECOND ? Math::ds : 1);
435  if (ind == AZIMUTH) {
436  angle = Math::AngNormalize(angle);
437  // Only angles strictly less than 0 can become 360; since +/-180 are
438  // folded together, we convert -0 to +0 (instead of 360).
439  if (angle < 0)
440  angle += Math::td;
441  else
442  angle = Math::real(0) + angle;
443  }
444  int sign = signbit(angle) ? -1 : 1;
445  angle *= sign;
446 
447  // Break off integer part to preserve precision and avoid overflow in
448  // manipulation of fractional part for MINUTE and SECOND
449  real
450  idegree = trailing == DEGREE ? 0 : floor(angle),
451  fdegree = (angle - idegree) * scale;
452  string s = Utility::str(fdegree, prec), degree, minute, second;
453  switch (trailing) {
454  case DEGREE:
455  degree = s;
456  break;
457  default: // case MINUTE: case SECOND:
458  string::size_type p = s.find_first_of('.');
459  long long i;
460  if (p == 0)
461  i = 0;
462  else {
463  i = stoll(s);
464  if (p == string::npos)
465  s.clear();
466  else
467  s = s.substr(p);
468  }
469  // Now i in [0,Math::dm] or [0,Math::ds] for MINUTE/DEGREE
470  switch (trailing) {
471  case MINUTE:
472  minute = to_string(i % Math::dm) + s; i /= Math::dm;
473  degree = Utility::str(i + idegree, 0); // no overflow since i in [0,1]
474  break;
475  default: // case SECOND:
476  second = to_string(i % Math::ms) + s; i /= Math::ms;
477  minute = to_string(i % Math::dm) ; i /= Math::dm;
478  degree = Utility::str(i + idegree, 0); // no overflow since i in [0,1]
479  break;
480  }
481  break;
482  }
483  // No glue together degree+minute+second with
484  // sign + zero-fill + delimiters + hemisphere
485  ostringstream str;
486  if (prec) ++prec; // Extra width for decimal point
487  if (ind == NONE && sign < 0)
488  str << '-';
489  str << setfill('0');
490  switch (trailing) {
491  case DEGREE:
492  if (ind != NONE)
493  str << setw(1 + min(int(ind), 2) + prec);
494  str << degree;
495  // Don't include degree designator (d) if it is the trailing component.
496  break;
497  case MINUTE:
498  if (ind != NONE)
499  str << setw(1 + min(int(ind), 2));
500  str << degree << (dmssep ? dmssep : char(tolower(dmsindicators_[0])))
501  << setw(2 + prec) << minute;
502  if (!dmssep)
503  str << char(tolower(dmsindicators_[1]));
504  break;
505  default: // case SECOND:
506  if (ind != NONE)
507  str << setw(1 + min(int(ind), 2));
508  str << degree << (dmssep ? dmssep : char(tolower(dmsindicators_[0])))
509  << setw(2)
510  << minute << (dmssep ? dmssep : char(tolower(dmsindicators_[1])))
511  << setw(2 + prec) << second;
512  if (!dmssep)
513  str << char(tolower(dmsindicators_[2]));
514  break;
515  }
516  if (ind != NONE && ind != AZIMUTH)
517  str << hemispheres_[(ind == LATITUDE ? 0 : 2) + (sign < 0 ? 0 : 1)];
518  return str.str();
519  }
520 
521 } // namespace GeographicLib
static int lookup(const std::string &s, char c)
Definition: Utility.cpp:160
static Math::real DecodeAngle(const std::string &angstr)
Definition: DMS.cpp:404
static constexpr int ds
seconds per degree
Definition: Math.hpp:147
Header for GeographicLib::Utility class.
static int extra_digits()
Definition: Math.cpp:49
static constexpr int dm
minutes per degree
Definition: Math.hpp:143
static std::string Encode(real angle, component trailing, unsigned prec, flag ind=NONE, char dmssep=char(0))
Definition: DMS.cpp:422
static T AngNormalize(T x)
Definition: Math.cpp:69
static Math::real DecodeAzimuth(const std::string &azistr)
Definition: DMS.cpp:413
static constexpr int qd
degrees per quarter turn
Definition: Math.hpp:142
static Math::real Decode(const std::string &dms, flag &ind)
Definition: DMS.cpp:40
Namespace for GeographicLib.
Definition: Accumulator.cpp:12
static std::string str(T x, int p=-1)
Definition: Utility.hpp:161
static void DecodeLatLon(const std::string &dmsa, const std::string &dmsb, real &lat, real &lon, bool longfirst=false)
Definition: DMS.cpp:374
GeographicLib::Math::real real
Definition: Geod3Solve.cpp:25
Exception handling for GeographicLib.
Definition: Constants.hpp:344
static constexpr int ms
seconds per minute
Definition: Math.hpp:144
GeographicLib::Angle ang
Definition: Geod3Solve.cpp:26
static constexpr int td
degrees per turn
Definition: Math.hpp:146
Header for GeographicLib::DMS class.