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