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