Strutils to format and parse SGGeod/lat/lon

Format portion is taken from code in fg_props, parsing code is my
own horrible concoction.
This commit is contained in:
James Turner 2018-06-20 22:00:58 +01:00
parent 99d30d5bb7
commit 9b1444deb5
3 changed files with 285 additions and 1 deletions

View File

@ -37,7 +37,7 @@
#include <simgear/package/md5.h>
#include <simgear/compiler.h> // SG_WINDOWS
#include <simgear/structure/exception.hxx>
#include <simgear/math/SGGeod.hxx>
#if defined(SG_WINDOWS)
#include <windows.h>
@ -1134,6 +1134,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<double>::round((min-int(min))*1000)), c);
} else {
snprintf(buf, sizeof(buf), "%03d* %02d'.%03d%c", int(deg), int(min), int(SGMisc<double>::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
} // end namespace simgear

View File

@ -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:
* <signed decimal degrees latitude>,<signed decimal degress longitude>
*/
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

View File

@ -20,6 +20,7 @@
#include <simgear/misc/strutils.hxx>
#include <simgear/structure/exception.hxx>
#include <simgear/constants.h>
#include <simgear/math/SGGeod.hxx>
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;
}