From 9b1444deb505be11ac32b8b179571b49e989be14 Mon Sep 17 00:00:00 2001 From: James Turner Date: Wed, 20 Jun 2018 22:00:58 +0100 Subject: [PATCH] Strutils to format and parse SGGeod/lat/lon Format portion is taken from code in fg_props, parsing code is my own horrible concoction. --- simgear/misc/strutils.cxx | 212 ++++++++++++++++++++++++++++++++- simgear/misc/strutils.hxx | 41 +++++++ simgear/misc/strutils_test.cxx | 33 +++++ 3 files changed, 285 insertions(+), 1 deletion(-) diff --git a/simgear/misc/strutils.cxx b/simgear/misc/strutils.cxx index e8117e6b..b0a75cbf 100644 --- a/simgear/misc/strutils.cxx +++ b/simgear/misc/strutils.cxx @@ -37,7 +37,7 @@ #include #include // SG_WINDOWS #include - +#include #if defined(SG_WINDOWS) #include @@ -1133,6 +1133,216 @@ bool matchPropPathToTemplate(const std::string& path, const std::string& templat // unreachable } + +bool parseStringAsLatLonValue(const std::string& s, double& degrees) +{ + string ss = simplify(s); + auto spacePos = ss.find(' '); + + if (spacePos == std::string::npos) { + degrees = std::stof(ss); + } else { + degrees = std::stof(ss.substr(0, spacePos)); + + double minutes = 0.0, seconds = 0.0; + + // check for minutes marker + auto quotePos = ss.find('\''); + if (quotePos == std::string::npos) { + minutes = std::stof(ss.substr(spacePos)); + } else { + minutes = std::stof(ss.substr(spacePos, quotePos - spacePos)); + seconds = std::stof(ss.substr(quotePos+1)); + } + + if ((seconds < 0.0) || (minutes < 0.0)) { + // don't allow sign information in minutes or seconds + return false; + } + + double offset = (minutes / 60.0) + (seconds / 3600.0); + degrees += (degrees >= 0.0) ? offset : -offset; + } + + // since we simplified, any trailing N/S/E/W must be the last char + const char lastChar = ss.back(); + if ((lastChar == 'W') || (lastChar == 'S')) { + degrees = -degrees; + } + + return true; +} + +bool parseStringAsGeod(const std::string& s, SGGeod* result) +{ + if (s.empty()) + return false; + + const auto commaPos = s.find(','); + if (commaPos == string::npos) { + return false; + } + + double lat, lon; + if (!parseStringAsLatLonValue(s.substr(0, commaPos), lat) || + !parseStringAsLatLonValue(s.substr(commaPos+1), lon)) + { + return false; + } + + if (result) { + *result = SGGeod::fromDeg(lon, lat); + } + return true; +} + +std::string formatLatLonValueAsString(double deg, LatLonFormat format, char c) +{ + double min, sec; + const int sign = deg < 0.0 ? -1 : 1; + deg = fabs(deg); + char buf[128]; + + switch (format) { + case LatLonFormat::DECIMAL_DEGREES: + ::snprintf(buf, sizeof(buf), "%3.6f%c", deg, c); + break; + + case LatLonFormat::DEGREES_MINUTES: + // d mm.mmm' (DMM format) -- uses a round-off factor tailored to the + // required precision of the minutes field (three decimal places), + // preventing minute values of 60. + min = (deg - int(deg)) * 60.0; + if (min >= 59.9995) { + min -= 60.0; + deg += 1.0; + } + snprintf(buf, sizeof(buf), "%d*%06.3f'%c", int(deg), fabs(min), c); + break; + + case LatLonFormat::DEGREES_MINUTES_SECONDS: + // d mm'ss.s" (DMS format) -- uses a round-off factor tailored to the + // required precision of the seconds field (one decimal place), + // preventing second values of 60. + min = (deg - int(deg)) * 60.0; + sec = (min - int(min)) * 60.0; + if (sec >= 59.95) { + sec -= 60.0; + min += 1.0; + if (min >= 60.0) { + min -= 60.0; + deg += 1.0; + } + } + ::snprintf(buf, sizeof(buf), "%d*%02d'%04.1f\"%c", int(deg), int(min), fabs(sec), c); + break; + + case LatLonFormat::SIGNED_DECIMAL_DEGREES: + // d.dddddd' (signed DDD format). + ::snprintf(buf, sizeof(buf), "%3.6f", sign*deg); + break; + + case LatLonFormat::SIGNED_DEGREES_MINUTES: + // d mm.mmm' (signed DMM format). + min = (deg - int(deg)) * 60.0; + if (min >= 59.9995) { + min -= 60.0; + deg += 1.0; + } + if (sign == 1) { + snprintf(buf, sizeof(buf), "%d*%06.3f'", int(deg), fabs(min)); + } else { + snprintf(buf, sizeof(buf), "-%d*%06.3f'", int(deg), fabs(min)); + } + break; + + + case LatLonFormat::SIGNED_DEGREES_MINUTES_SECONDS: + // d mm'ss.s" (signed DMS format). + min = (deg - int(deg)) * 60.0; + sec = (min - int(min)) * 60.0; + if (sec >= 59.95) { + sec -= 60.0; + min += 1.0; + if (min >= 60.0) { + min -= 60.0; + deg += 1.0; + } + } + if (sign == 1) { + snprintf(buf, sizeof(buf), "%d*%02d'%04.1f\"", int(deg), int(min), fabs(sec)); + } else { + snprintf(buf, sizeof(buf), "-%d*%02d'%04.1f\"", int(deg), int(min), fabs(sec)); + } + break; + + case LatLonFormat::ZERO_PAD_DECIMAL_DEGRESS: + // dd.dddddd X, ddd.dddddd X (zero padded DDD format). + if (c == 'N' || c == 'S') { + snprintf(buf, sizeof(buf), "%09.6f%c", deg, c); + } else { + snprintf(buf, sizeof(buf), "%010.6f%c", deg, c); + } + break; + + case LatLonFormat::ZERO_PAD_DEGREES_MINUTES: + // dd mm.mmm' X, ddd mm.mmm' X (zero padded DMM format). + min = (deg - int(deg)) * 60.0; + if (min >= 59.9995) { + min -= 60.0; + deg += 1.0; + } + if (c == 'N' || c == 'S') { + snprintf(buf, sizeof(buf), "%02d*%06.3f'%c", int(deg), fabs(min), c); + } else { + snprintf(buf, sizeof(buf), "%03d*%06.3f'%c", int(deg), fabs(min), c); + } + break; + + case LatLonFormat::ZERO_PAD_DEGREES_MINUTES_SECONDS: + // dd mm'ss.s" X, dd mm'ss.s" X (zero padded DMS format). + min = (deg - int(deg)) * 60.0; + sec = (min - int(min)) * 60.0; + if (sec >= 59.95) { + sec -= 60.0; + min += 1.0; + if (min >= 60.0) { + min -= 60.0; + deg += 1.0; + } + } + if (c == 'N' || c == 'S') { + snprintf(buf, sizeof(buf), "%02d*%02d'%04.1f\"%c", int(deg), int(min), fabs(sec), c); + } else { + snprintf(buf, sizeof(buf), "%03d*%02d'%04.1f\"%c", int(deg), int(min), fabs(sec), c); + } + break; + + case LatLonFormat::TRINITY_HOUSE: + // dd* mm'.mmm X, ddd* mm'.mmm X (Trinity House Navigation standard). + min = (deg - int(deg)) * 60.0; + if (min >= 59.9995) { + min -= 60.0; + deg += 1.0; + } + if (c == 'N' || c == 'S') { + snprintf(buf, sizeof(buf), "%02d* %02d'.%03d%c", int(deg), int(min), int(SGMisc::round((min-int(min))*1000)), c); + } else { + snprintf(buf, sizeof(buf), "%03d* %02d'.%03d%c", int(deg), int(min), int(SGMisc::round((min-int(min))*1000)), c); + } + break; + } + + return std::string(buf); +} + +std::string formatGeodAsString(const SGGeod& geod, LatLonFormat format) +{ + const char ns = (geod.getLatitudeDeg() > 0.0) ? 'N' : 'S'; + const char ew = (geod.getLongitudeDeg() > 0.0) ? 'E' : 'W'; + + return formatLatLonValueAsString(geod.getLatitudeDeg(), format, ns) + "," + formatLatLonValueAsString(geod.getLongitudeDeg(), format, ew); +} } // end namespace strutils diff --git a/simgear/misc/strutils.hxx b/simgear/misc/strutils.hxx index ec464a5e..e17244fe 100644 --- a/simgear/misc/strutils.hxx +++ b/simgear/misc/strutils.hxx @@ -36,6 +36,9 @@ typedef std::vector < std::string > string_list; +// forward decls +class SGGeod; + namespace simgear { namespace strutils { @@ -354,6 +357,44 @@ namespace simgear { * /views[0]/view[4]/fig, /views[0]/view[1000]/flight */ bool matchPropPathToTemplate(const std::string& path, const std::string& templatePath); + + bool parseStringAsLatLonValue(const std::string& s, double& result); + + /** + * Attempt to parse a string as a latitude,longitude input. Returns true + * or false based on success, and returns the SGGeod by pointer. Leading, + * trailing and internal white-space is skipped / ignored. + * + * Supported formats: + * , + */ + bool parseStringAsGeod(const std::string& string, SGGeod* result = nullptr); + + // enum values here correspond to existing lon-lat format codes inside + // FlightGear (property: /sim/lon-lat-format ) + // Don't re-order, just add new ones, or things may break + enum class LatLonFormat + { + DECIMAL_DEGREES = 0, ///< 88.4N,4.54W, + DEGREES_MINUTES, ///< 88 24.6'N, 4 30.5'W + DEGREES_MINUTES_SECONDS, + SIGNED_DECIMAL_DEGREES, ///< 88.4,-4.54 + SIGNED_DEGREES_MINUTES, + SIGNED_DEGREES_MINUTES_SECONDS, + ZERO_PAD_DECIMAL_DEGRESS, + ZERO_PAD_DEGREES_MINUTES, + ZERO_PAD_DEGREES_MINUTES_SECONDS, + TRINITY_HOUSE, ///< dd* mm'.mmm X, ddd* mm'.mmm X (Trinity House Navigation standard). + }; + + std::string formatLatLonValueAsString(double deg, LatLonFormat format, char c); + + /** + * Format an SGGeod as a string according to the provided rule. + * if the SGGeod is invalid (default constructed), will return an empty string + */ + std::string formatGeodAsString(const SGGeod& geod, + LatLonFormat format = LatLonFormat::DECIMAL_DEGREES); } // end namespace strutils } // end namespace simgear diff --git a/simgear/misc/strutils_test.cxx b/simgear/misc/strutils_test.cxx index 3a5f2e72..c7c13dd2 100644 --- a/simgear/misc/strutils_test.cxx +++ b/simgear/misc/strutils_test.cxx @@ -20,6 +20,7 @@ #include #include #include +#include using std::string; using std::vector; @@ -619,6 +620,36 @@ void test_utf8Convert() SG_VERIFY(a == aRoundTrip); } +void test_parseGeod() +{ + SGGeod a; + SG_VERIFY(strutils::parseStringAsGeod("56.12,-3.0", &a)); + SG_CHECK_EQUAL_EP(a.getLongitudeDeg(), -3.0); + SG_CHECK_EQUAL_EP2(a.getLatitudeDeg(), 56.12, 1e-4); + + // embedded whitepace, DMS notation, NSEW notation + SG_VERIFY(strutils::parseStringAsGeod("\t40 30'50\"S, 12 34'56\"W ", &a)); + SG_CHECK_EQUAL_EP2(a.getLongitudeDeg(), -12.58222222, 1e-4); + SG_CHECK_EQUAL_EP2(a.getLatitudeDeg(), -40.5138888, 1e-4); + + // signed degrees-minutes + SG_VERIFY(strutils::parseStringAsGeod("-45 27.89,-12 34.56", &a)); + SG_CHECK_EQUAL_EP2(a.getLongitudeDeg(), -12.576, 1e-4); + SG_CHECK_EQUAL_EP2(a.getLatitudeDeg(), -45.464833, 1e-4); + + SG_VERIFY(strutils::parseStringAsGeod("") == false); + SG_VERIFY(strutils::parseStringAsGeod("aaaaaaaa") == false); +} + +void test_formatGeod() +{ + SGGeod a = SGGeod::fromDeg(-3.46, 55.45); + SG_CHECK_EQUAL(strutils::formatGeodAsString(a, strutils::LatLonFormat::SIGNED_DECIMAL_DEGREES), "55.450000,-3.460000"); + SG_CHECK_EQUAL(strutils::formatGeodAsString(a, strutils::LatLonFormat::DEGREES_MINUTES_SECONDS), + "55*27'00.0\"N,3*27'36.0\"W"); + +} + int main(int argc, char* argv[]) { test_strip(); @@ -639,6 +670,8 @@ int main(int argc, char* argv[]) test_propPathMatch(); test_readTime(); test_utf8Convert(); + test_parseGeod(); + test_formatGeod(); return EXIT_SUCCESS; }