DMS.cpp
Go to the documentation of this file.
/**
* \file DMS.cpp
* \brief Implementation for GeographicLib::DMS class
*
* Copyright (c) Charles Karney (2008-2022) <karney@alum.mit.edu> and licensed
* under the MIT/X11 License. For more information, see
* https://geographiclib.sourceforge.io/
**********************************************************************/
#include <GeographicLib/DMS.hpp>
#include <GeographicLib/Utility.hpp>
#if defined(_MSC_VER)
// Squelch warnings about constant conditional and enum-float expressions
# pragma warning (disable: 4127 5055)
#endif
namespace GeographicLib {
using namespace std;
const char* const DMS::hemispheres_ = "SNWE";
const char* const DMS::signs_ = "-+";
const char* const DMS::digits_ = "0123456789";
const char* const DMS::dmsindicators_ = "D'\":";
const char* const DMS::components_[] = {"degrees", "minutes", "seconds"};
// Replace all occurrences of pat by c. If c is NULL remove pat.
void DMS::replace(std::string& s, const std::string& pat, char c) {
string::size_type p = 0;
int count = c ? 1 : 0;
while (true) {
p = s.find(pat, p);
if (p == string::npos)
break;
s.replace(p, pat.length(), count, c);
}
}
Math::real DMS::Decode(const std::string& dms, flag& ind) {
// Here's a table of the allowed characters
// S unicode dec UTF-8 descripton
// DEGREE
// d U+0064 100 64 d
// D U+0044 68 44 D
// ° U+00b0 176 c2 b0 degree symbol
// º U+00ba 186 c2 ba alt symbol
// ⁰ U+2070 8304 e2 81 b0 sup zero
// ˚ U+02da 730 cb 9a ring above
// ∘ U+2218 8728 e2 88 98 compose function
// * U+002a 42 2a GRiD symbol for degrees
// MINUTES
// ' U+0027 39 27 apostrophe
// ` U+0060 96 60 grave accent
// ′ U+2032 8242 e2 80 b2 prime
// ‵ U+2035 8245 e2 80 b5 back prime
// ´ U+00b4 180 c2 b4 acute accent
// ‘ U+2018 8216 e2 80 98 left single quote (also ext ASCII 0x91)
// ’ U+2019 8217 e2 80 99 right single quote (also ext ASCII 0x92)
// ‛ U+201b 8219 e2 80 9b reversed-9 single quote
// ʹ U+02b9 697 ca b9 modifier letter prime
// ˊ U+02ca 714 cb 8a modifier letter acute accent
// ˋ U+02cb 715 cb 8b modifier letter grave accent
// SECONDS
// " U+0022 34 22 quotation mark
// ″ U+2033 8243 e2 80 b3 double prime
// ‶ U+2036 8246 e2 80 b6 reversed double prime
// ˝ U+02dd 733 cb 9d double acute accent
// “ U+201c 8220 e2 80 9c left double quote (also ext ASCII 0x93)
// ” U+201d 8221 e2 80 9d right double quote (also ext ASCII 0x94)
// ‟ U+201f 8223 e2 80 9f reversed-9 double quote
// ʺ U+02ba 698 ca ba modifier letter double prime
// PLUS
// + U+002b 43 2b plus sign
// ➕ U+2795 10133 e2 9e 95 heavy plus
// U+2064 8292 e2 81 a4 invisible plus |⁤|
// MINUS
// - U+002d 45 2d hyphen
// ‐ U+2010 8208 e2 80 90 dash
// ‑ U+2011 8209 e2 80 91 non-breaking hyphen
// – U+2013 8211 e2 80 93 en dash (also ext ASCII 0x96)
// — U+2014 8212 e2 80 94 em dash (also ext ASCII 0x97)
// − U+2212 8722 e2 88 92 minus sign
// ➖ U+2796 10134 e2 9e 96 heavy minus
// IGNORED
// U+00a0 160 c2 a0 non-breaking space
// U+2007 8199 e2 80 87 figure space | |
// U+2009 8201 e2 80 89 thin space | |
// U+200a 8202 e2 80 8a hair space | |
// U+200b 8203 e2 80 8b invisible space |​|
//   U+202f 8239 e2 80 af narrow space | |
// U+2063 8291 e2 81 a3 invisible separator |⁣|
// « U+00ab 171 c2 ab left guillemot (for cgi-bin)
// » U+00bb 187 c2 bb right guillemot (for cgi-bin)
string dmsa = dms;
replace(dmsa, "\xc2\xb0", 'd' ); // U+00b0 degree symbol
replace(dmsa, "\xc2\xba", 'd' ); // U+00ba alt symbol
replace(dmsa, "\xe2\x81\xb0", 'd' ); // U+2070 sup zero
replace(dmsa, "\xcb\x9a", 'd' ); // U+02da ring above
replace(dmsa, "\xe2\x88\x98", 'd' ); // U+2218 compose function
replace(dmsa, "\xe2\x80\xb2", '\''); // U+2032 prime
replace(dmsa, "\xe2\x80\xb5", '\''); // U+2035 back prime
replace(dmsa, "\xc2\xb4", '\''); // U+00b4 acute accent
replace(dmsa, "\xe2\x80\x98", '\''); // U+2018 left single quote
replace(dmsa, "\xe2\x80\x99", '\''); // U+2019 right single quote
replace(dmsa, "\xe2\x80\x9b", '\''); // U+201b reversed-9 single quote
replace(dmsa, "\xca\xb9", '\''); // U+02b9 modifier letter prime
replace(dmsa, "\xcb\x8a", '\''); // U+02ca modifier letter acute accent
replace(dmsa, "\xcb\x8b", '\''); // U+02cb modifier letter grave accent
replace(dmsa, "\xe2\x80\xb3", '"' ); // U+2033 double prime
replace(dmsa, "\xe2\x80\xb6", '"' ); // U+2036 reversed double prime
replace(dmsa, "\xcb\x9d", '"' ); // U+02dd double acute accent
replace(dmsa, "\xe2\x80\x9c", '"' ); // U+201c left double quote
replace(dmsa, "\xe2\x80\x9d", '"' ); // U+201d right double quote
replace(dmsa, "\xe2\x80\x9f", '"' ); // U+201f reversed-9 double quote
replace(dmsa, "\xca\xba", '"' ); // U+02ba modifier letter double prime
replace(dmsa, "\xe2\x9e\x95", '+' ); // U+2795 heavy plus
replace(dmsa, "\xe2\x81\xa4", '+' ); // U+2064 invisible plus
replace(dmsa, "\xe2\x80\x90", '-' ); // U+2010 dash
replace(dmsa, "\xe2\x80\x91", '-' ); // U+2011 non-breaking hyphen
replace(dmsa, "\xe2\x80\x93", '-' ); // U+2013 en dash
replace(dmsa, "\xe2\x80\x94", '-' ); // U+2014 em dash
replace(dmsa, "\xe2\x88\x92", '-' ); // U+2212 minus sign
replace(dmsa, "\xe2\x9e\x96", '-' ); // U+2796 heavy minus
replace(dmsa, "\xc2\xa0", '\0'); // U+00a0 non-breaking space
replace(dmsa, "\xe2\x80\x87", '\0'); // U+2007 figure space
replace(dmsa, "\xe2\x80\x89", '\0'); // U+2007 thin space
replace(dmsa, "\xe2\x80\x8a", '\0'); // U+200a hair space
replace(dmsa, "\xe2\x80\x8b", '\0'); // U+200b invisible space
replace(dmsa, "\xe2\x80\xaf", '\0'); // U+202f narrow space
replace(dmsa, "\xe2\x81\xa3", '\0'); // U+2063 invisible separator
replace(dmsa, "\xb0", 'd' ); // 0xb0 bare degree symbol
replace(dmsa, "\xba", 'd' ); // 0xba bare alt symbol
replace(dmsa, "*", 'd' ); // GRiD symbol for degree
replace(dmsa, "`", '\''); // grave accent
replace(dmsa, "\xb4", '\''); // 0xb4 bare acute accent
// Don't implement these alternatives; they are only relevant for cgi-bin
// replace(dmsa, "\x91", '\''); // 0x91 ext ASCII left single quote
// replace(dmsa, "\x92", '\''); // 0x92 ext ASCII right single quote
// replace(dmsa, "\x93", '"' ); // 0x93 ext ASCII left double quote
// replace(dmsa, "\x94", '"' ); // 0x94 ext ASCII right double quote
// replace(dmsa, "\x96", '-' ); // 0x96 ext ASCII en dash
// replace(dmsa, "\x97", '-' ); // 0x97 ext ASCII em dash
replace(dmsa, "\xa0", '\0'); // 0xa0 bare non-breaking space
replace(dmsa, "''", '"' ); // '' -> "
string::size_type
beg = 0,
end = unsigned(dmsa.size());
while (beg < end && isspace(dmsa[beg]))
++beg;
while (beg < end && isspace(dmsa[end - 1]))
--end;
// The trimmed string in [beg, end)
real v = -0.0; // So "-0" returns -0.0
int i = 0;
flag ind1 = NONE;
// p is pointer to the next piece that needs decoding
for (string::size_type p = beg, pb; p < end; p = pb, ++i) {
string::size_type pa = p;
// Skip over initial hemisphere letter (for i == 0)
if (i == 0 && Utility::lookup(hemispheres_, dmsa[pa]) >= 0)
++pa;
// Skip over initial sign (checking for it if i == 0)
if (i > 0 || (pa < end && Utility::lookup(signs_, dmsa[pa]) >= 0))
++pa;
// Find next sign
pb = min(dmsa.find_first_of(signs_, pa), end);
flag ind2 = NONE;
v += InternalDecode(dmsa.substr(p, pb - p), ind2);
if (ind1 == NONE)
ind1 = ind2;
else if (!(ind2 == NONE || ind1 == ind2))
throw GeographicErr("Incompatible hemisphere specifier in " +
dmsa.substr(beg, pb - beg));
}
if (i == 0)
throw GeographicErr("Empty or incomplete DMS string " +
dmsa.substr(beg, end - beg));
ind = ind1;
return v;
}
Math::real DMS::InternalDecode(const string& dmsa, flag& ind) {
string errormsg;
do { // Executed once (provides the ability to break)
int sign = 1;
unsigned
beg = 0,
end = unsigned(dmsa.size());
flag ind1 = NONE;
int k = -1;
if (end > beg && (k = Utility::lookup(hemispheres_, dmsa[beg])) >= 0) {
ind1 = (k / 2) ? LONGITUDE : LATITUDE;
sign = k % 2 ? 1 : -1;
++beg;
}
if (end > beg && (k = Utility::lookup(hemispheres_, dmsa[end-1])) >= 0) {
if (k >= 0) {
if (ind1 != NONE) {
if (toupper(dmsa[beg - 1]) == toupper(dmsa[end - 1]))
errormsg = "Repeated hemisphere indicators "
+ Utility::str(dmsa[beg - 1])
+ " in " + dmsa.substr(beg - 1, end - beg + 1);
else
errormsg = "Contradictory hemisphere indicators "
+ Utility::str(dmsa[beg - 1]) + " and "
+ Utility::str(dmsa[end - 1]) + " in "
+ dmsa.substr(beg - 1, end - beg + 1);
break;
}
ind1 = (k / 2) ? LONGITUDE : LATITUDE;
sign = k % 2 ? 1 : -1;
--end;
}
}
if (end > beg && (k = Utility::lookup(signs_, dmsa[beg])) >= 0) {
if (k >= 0) {
sign *= k ? 1 : -1;
++beg;
}
}
if (end == beg) {
errormsg = "Empty or incomplete DMS string " + dmsa;
break;
}
real ipieces[] = {0, 0, 0};
real fpieces[] = {0, 0, 0};
unsigned npiece = 0;
real icurrent = 0;
real fcurrent = 0;
unsigned ncurrent = 0, p = beg;
bool pointseen = false;
unsigned digcount = 0, intcount = 0;
while (p < end) {
char x = dmsa[p++];
if ((k = Utility::lookup(digits_, x)) >= 0) {
++ncurrent;
if (digcount > 0)
++digcount; // Count of decimal digits
else {
icurrent = 10 * icurrent + k;
++intcount;
}
} else if (x == '.') {
if (pointseen) {
errormsg = "Multiple decimal points in "
+ dmsa.substr(beg, end - beg);
break;
}
pointseen = true;
digcount = 1;
} else if ((k = Utility::lookup(dmsindicators_, x)) >= 0) {
if (k >= 3) {
if (p == end) {
errormsg = "Illegal for : to appear at the end of " +
dmsa.substr(beg, end - beg);
break;
}
k = npiece;
}
if (unsigned(k) == npiece - 1) {
errormsg = "Repeated " + string(components_[k]) +
" component in " + dmsa.substr(beg, end - beg);
break;
} else if (unsigned(k) < npiece) {
errormsg = string(components_[k]) + " component follows "
+ string(components_[npiece - 1]) + " component in "
+ dmsa.substr(beg, end - beg);
break;
}
if (ncurrent == 0) {
errormsg = "Missing numbers in " + string(components_[k]) +
" component of " + dmsa.substr(beg, end - beg);
break;
}
if (digcount > 0) {
istringstream s(dmsa.substr(p - intcount - digcount - 1,
intcount + digcount));
s >> fcurrent;
icurrent = 0;
}
ipieces[k] = icurrent;
fpieces[k] = icurrent + fcurrent;
if (p < end) {
npiece = k + 1;
icurrent = fcurrent = 0;
ncurrent = digcount = intcount = 0;
}
} else if (Utility::lookup(signs_, x) >= 0) {
errormsg = "Internal sign in DMS string "
+ dmsa.substr(beg, end - beg);
break;
} else {
errormsg = "Illegal character " + Utility::str(x) + " in DMS string "
+ dmsa.substr(beg, end - beg);
break;
}
}
if (!errormsg.empty())
break;
if (Utility::lookup(dmsindicators_, dmsa[p - 1]) < 0) {
if (npiece >= 3) {
errormsg = "Extra text following seconds in DMS string "
+ dmsa.substr(beg, end - beg);
break;
}
if (ncurrent == 0) {
errormsg = "Missing numbers in trailing component of "
+ dmsa.substr(beg, end - beg);
break;
}
if (digcount > 0) {
istringstream s(dmsa.substr(p - intcount - digcount,
intcount + digcount));
s >> fcurrent;
icurrent = 0;
}
ipieces[npiece] = icurrent;
fpieces[npiece] = icurrent + fcurrent;
}
if (pointseen && digcount == 0) {
errormsg = "Decimal point in non-terminal component of "
+ dmsa.substr(beg, end - beg);
break;
}
// Note that we accept 59.999999... even though it rounds to 60.
if (ipieces[1] >= Math::dm || fpieces[1] > Math::dm ) {
errormsg = "Minutes " + Utility::str(fpieces[1])
+ " not in range [0, " + to_string(Math::dm) + ")";
break;
}
if (ipieces[2] >= Math::ms || fpieces[2] > Math::ms) {
errormsg = "Seconds " + Utility::str(fpieces[2])
+ " not in range [0, " + to_string(Math::ms) + ")";
break;
}
ind = ind1;
// Assume check on range of result is made by calling routine (which
// might be able to offer a better diagnostic).
return real(sign) *
( fpieces[2] != 0 ?
(Math::ms*(Math::dm*fpieces[0] + fpieces[1]) + fpieces[2])/Math::ds :
( fpieces[1] != 0 ?
(Math::dm*fpieces[0] + fpieces[1]) / Math::dm : fpieces[0] ) );
} while (false);
real val = Utility::nummatch<real>(dmsa);
if (val == 0)
throw GeographicErr(errormsg);
else
ind = NONE;
return val;
}
void DMS::DecodeLatLon(const string& stra, const string& strb,
real& lat, real& lon,
bool longfirst) {
real a, b;
flag ia, ib;
a = Decode(stra, ia);
b = Decode(strb, ib);
if (ia == NONE && ib == NONE) {
// Default to lat, long unless longfirst
ia = longfirst ? LONGITUDE : LATITUDE;
ib = longfirst ? LATITUDE : LONGITUDE;
} else if (ia == NONE)
ia = flag(LATITUDE + LONGITUDE - ib);
else if (ib == NONE)
ib = flag(LATITUDE + LONGITUDE - ia);
if (ia == ib)
throw GeographicErr("Both " + stra + " and "
+ strb + " interpreted as "
+ (ia == LATITUDE ? "latitudes" : "longitudes"));
real
lat1 = ia == LATITUDE ? a : b,
lon1 = ia == LATITUDE ? b : a;
if (fabs(lat1) > Math::qd)
throw GeographicErr("Latitude " + Utility::str(lat1)
+ "d not in [-" + to_string(Math::qd)
+ "d, " + to_string(Math::qd) + "d]");
lat = lat1;
lon = lon1;
}
Math::real DMS::DecodeAngle(const string& angstr) {
flag ind;
real ang = Decode(angstr, ind);
if (ind != NONE)
throw GeographicErr("Arc angle " + angstr
+ " includes a hemisphere, N/E/W/S");
return ang;
}
Math::real DMS::DecodeAzimuth(const string& azistr) {
flag ind;
real azi = Decode(azistr, ind);
if (ind == LATITUDE)
throw GeographicErr("Azimuth " + azistr
+ " has a latitude hemisphere, N/S");
return Math::AngNormalize(azi);
}
string DMS::Encode(real angle, component trailing, unsigned prec, flag ind,
char dmssep) {
// Assume check on range of input angle has been made by calling
// routine (which might be able to offer a better diagnostic).
if (!isfinite(angle))
return angle < 0 ? string("-inf") :
(angle > 0 ? string("inf") : string("nan"));
// 15 - 2 * trailing = ceiling(log10(2^53/90/60^trailing)).
// This suffices to give full real precision for numbers in [-90,90]
prec = min(15 + Math::extra_digits() - 2 * unsigned(trailing), prec);
real scale = trailing == MINUTE ? Math::dm :
(trailing == SECOND ? Math::ds : 1);
if (ind == AZIMUTH) {
angle = Math::AngNormalize(angle);
// Only angles strictly less than 0 can become 360; since +/-180 are
// folded together, we convert -0 to +0 (instead of 360).
if (angle < 0)
angle += Math::td;
else
angle = Math::real(0) + angle;
}
int sign = signbit(angle) ? -1 : 1;
angle *= sign;
// Break off integer part to preserve precision and avoid overflow in
// manipulation of fractional part for MINUTE and SECOND
real
idegree = trailing == DEGREE ? 0 : floor(angle),
fdegree = (angle - idegree) * scale;
string s = Utility::str(fdegree, prec), degree, minute, second;
switch (trailing) {
case DEGREE:
degree = s;
break;
default: // case MINUTE: case SECOND:
string::size_type p = s.find_first_of('.');
long long i;
if (p == 0)
i = 0;
else {
i = stoll(s);
if (p == string::npos)
s.clear();
else
s = s.substr(p);
}
// Now i in [0,Math::dm] or [0,Math::ds] for MINUTE/DEGREE
switch (trailing) {
case MINUTE:
minute = to_string(i % Math::dm) + s; i /= Math::dm;
degree = Utility::str(i + idegree, 0); // no overflow since i in [0,1]
break;
default: // case SECOND:
second = to_string(i % Math::ms) + s; i /= Math::ms;
minute = to_string(i % Math::dm) ; i /= Math::dm;
degree = Utility::str(i + idegree, 0); // no overflow since i in [0,1]
break;
}
break;
}
// No glue together degree+minute+second with
// sign + zero-fill + delimiters + hemisphere
ostringstream str;
if (prec) ++prec; // Extra width for decimal point
if (ind == NONE && sign < 0)
str << '-';
str << setfill('0');
switch (trailing) {
case DEGREE:
if (ind != NONE)
str << setw(1 + min(int(ind), 2) + prec);
str << degree;
// Don't include degree designator (d) if it is the trailing component.
break;
case MINUTE:
if (ind != NONE)
str << setw(1 + min(int(ind), 2));
str << degree << (dmssep ? dmssep : char(tolower(dmsindicators_[0])))
<< setw(2 + prec) << minute;
if (!dmssep)
str << char(tolower(dmsindicators_[1]));
break;
default: // case SECOND:
if (ind != NONE)
str << setw(1 + min(int(ind), 2));
str << degree << (dmssep ? dmssep : char(tolower(dmsindicators_[0])))
<< setw(2)
<< minute << (dmssep ? dmssep : char(tolower(dmsindicators_[1])))
<< setw(2 + prec) << second;
if (!dmssep)
str << char(tolower(dmsindicators_[2]));
break;
}
if (ind != NONE && ind != AZIMUTH)
str << hemispheres_[(ind == LATITUDE ? 0 : 2) + (sign < 0 ? 0 : 1)];
return str.str();
}
} // namespace GeographicLib
DMS.hpp
Header for GeographicLib::DMS class.
real
GeographicLib::Math::real real
Definition: GeodSolve.cpp:29
Utility.hpp
Header for GeographicLib::Utility class.
GeographicLib::DMS::DecodeAzimuth
static Math::real DecodeAzimuth(const std::string &azistr)
Definition: DMS.cpp:407
GeographicLib::DMS::flag
flag
Definition: DMS.hpp:41
GeographicLib::DMS::LONGITUDE
@ LONGITUDE
Definition: DMS.hpp:56
GeographicLib::DMS::LATITUDE
@ LATITUDE
Definition: DMS.hpp:51
GeographicLib::DMS::AZIMUTH
@ AZIMUTH
Definition: DMS.hpp:62
GeographicLib::DMS::NONE
@ NONE
Definition: DMS.hpp:46
GeographicLib::DMS::DecodeAngle
static Math::real DecodeAngle(const std::string &angstr)
Definition: DMS.cpp:398
GeographicLib::DMS::component
component
Definition: DMS.hpp:73
GeographicLib::DMS::SECOND
@ SECOND
Definition: DMS.hpp:88
GeographicLib::DMS::DEGREE
@ DEGREE
Definition: DMS.hpp:78
GeographicLib::DMS::MINUTE
@ MINUTE
Definition: DMS.hpp:83
GeographicLib::DMS::Encode
static std::string Encode(real angle, component trailing, unsigned prec, flag ind=NONE, char dmssep=char(0))
Definition: DMS.cpp:416
GeographicLib::DMS::DecodeLatLon
static void DecodeLatLon(const std::string &dmsa, const std::string &dmsb, real &lat, real &lon, bool longfirst=false)
Definition: DMS.cpp:368
GeographicLib::DMS::Decode
static Math::real Decode(const std::string &dms, flag &ind)
Definition: DMS.cpp:40
GeographicLib::GeographicErr
Exception handling for GeographicLib.
Definition: Constants.hpp:316
GeographicLib::Math::AngNormalize
static T AngNormalize(T x)
Definition: Math.cpp:71
GeographicLib::Math::real
double real
Definition: Math.hpp:100
GeographicLib::Math::dm
@ dm
minutes per degree
Definition: Math.hpp:139
GeographicLib::Math::td
@ td
degrees per turn
Definition: Math.hpp:142
GeographicLib::Math::ms
@ ms
seconds per minute
Definition: Math.hpp:140
GeographicLib::Math::ds
@ ds
seconds per degree
Definition: Math.hpp:143
GeographicLib::Math::qd
@ qd
degrees per quarter turn
Definition: Math.hpp:138
GeographicLib::Math::extra_digits
static int extra_digits()
Definition: Math.cpp:51
GeographicLib::Utility::lookup
static int lookup(const std::string &s, char c)
Definition: Utility.cpp:160
GeographicLib::Utility::str
static std::string str(T x, int p=-1)
Definition: Utility.hpp:161
GeographicLib
Namespace for GeographicLib.
Definition: Accumulator.cpp:12
std
Definition: NearestNeighbor.hpp:812