diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 48fd478..aeefae1 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -2,9 +2,19 @@ name: Test (CMake) on: push: - branches: [ "master" ] + branches: + - 'master' + - '*' + paths-ignore: + - .gitignore + - README.md pull_request: - branches: [ "master" ] + branches: + - 'master' + - '*' + workflow_dispatch: + branches: + - '*' env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) @@ -21,7 +31,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Configure CMake if: matrix.os != 'windows-latest' @@ -74,7 +84,7 @@ jobs: # Upload coverage report only for Linux - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 50c0ab3..1d8adb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,8 +73,8 @@ endif() add_subdirectory(src) if (ENABLE_TESTING) + include(CTest) enable_testing() - include(Dart) add_subdirectory(test) endif() diff --git a/README.md b/README.md index 2c22a23..e9f00da 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,22 @@ int main(int argc, char **argv) } ``` +#### Conversion from/to strings + +Library supports latitude/longitude coordinates conversion +in three formats: Degrees Minutes Seconds *(D° M' S")*, +Decimal Minutes *(D° M.M')*, and Decimal Degrees *(D.D°)*. + +```cpp +auto lon = Longitude::fromString("45° 46’ 47.36” W"); +auto lat = Latitude::fromString("45°46′ 45.36″ N"); +``` + +```cpp +auto lon = Longitude{45.7790}; +auto lonStr = lon.toString(Coordinate::Format::DMS); // 45° 46′ 45.36″ E +``` + For more usage examples please refer to the unit tests at `/test/test.cpp` file. ### Building and Testing diff --git a/include/coordinate.h b/include/coordinate.h index 126f585..dffb391 100644 --- a/include/coordinate.h +++ b/include/coordinate.h @@ -27,6 +27,8 @@ #include "export.h" +#include + namespace erkir { @@ -34,9 +36,34 @@ namespace erkir class ERKIR_EXPORT Coordinate { public: + /// The coordinates human readable format types. + enum class Format + { + DMS, ///< Degrees Minutes Seconds (D° M' S") + DDM, ///< Decimal Minutes (D° M.M') + DD ///< Decimal Degrees (D.D°) + }; + + /// The cardinal points types + enum class CompassPrecision + { + ///< Main compass directions: north (N), south (S), east (E), west (W) + Cardinal, + ///< Ordinal directions: northeast (NE), southeast (SE), southwest (SW), northwest (NW) + Intercardinal, + /*!< Secondary intercardinal directions: north - northeast(NNE), east - northeast(ENE), + east - southeast(ESE), south - southeast(SSE), south - southwest(SSW), + west - southwest(WSW), west - northwest(WNW), north - northwest(NNW) + */ + SecondaryIntercardinal + }; + //! Constructs a coordinate by the given decimal degrees. Coordinate(double degrees); + /// Returns string representation of the coordinate in the specified \p format. + virtual std::string toString(Format format, int precision) const = 0; + //! Returns this coordinate's value in decimal degrees. double degrees() const; @@ -54,6 +81,29 @@ class ERKIR_EXPORT Coordinate /// Constrain degrees to range 0..360.0 (e.g. for bearings); -1 => 359, 361 => 1. static double wrap360(double degrees); + /// Returns compass point (to given precision) for supplied bearing. + /*! + 16-wind compass rose supported - the eight principal winds and the eight half-winds + together form the 16-wind compass rose, with each compass point at a 22.5° + angle from its two neighbours. The half-winds are north-northeast (NNE), + east-northeast (ENE), east-southeast (ESE), south-southeast (SSE), + south-southwest (SSW), west-southwest (WSW), west-northwest (WNW), + and north-northwest (NNW). + + @param bearing Bearing in degrees from north. + @param precision Precision (Cardinal, Intercardinal, SecondaryIntercardinal). + @returns Compass point for supplied bearing. + + @example + const point = Coordinate::compassPoint(24); // point = 'NNE' + const point = Coordinate::compassPoint(24, 1); // point = 'N' + */ + static std::string compassPoint(double bearing, + CompassPrecision precision = CompassPrecision::SecondaryIntercardinal); + +protected: + std::string toBaseString(Format format, int precision) const; + private: double m_degrees; }; @@ -69,6 +119,30 @@ class ERKIR_EXPORT Latitude : public Coordinate \throws std::out_of_range */ Latitude(double degree); + + std::string toString(Format format, int precision = 2) const override; + + /// Returns the latitude from the human readable coordinates (formatted). + /*! + Latitude/Longitude coordinates in three formats: Degrees Minutes Seconds (D° M' S"), + Decimal Minutes (D° M.M'), and Decimal Degrees (D.D°). Each of these formats + can represent the same geographic location, but expressed differently. + + For example: 45° 46' 52" N 108° 30' 14" W as displayed in Degrees Minutes Seconds (D° M' S"). + This same location, in displayed Decimal Minutes (D° M.M'), is: 45° 46.8666' N 108° 30.2333' W. + In Decimal Degrees (D.D°), this same location is: 45.7811111° N 108.5038888° W + + About Sign and North, South, East West + --------------------------------------------------------------------------- + Latitude/Longitude is followed by an indication of hemisphere. + For example 45° 46' 52" N indicates the Northern Hemisphere (North of the equator.) + 108° 30' 14" W indicates an area West of the Prime Meridian. When noting this numerically + (especially in Decimal Degrees), positive and negative values are sometimes used. + A positive value for North and East, a negative value for South and West. + Thus, in our example, when noting 45° 46' 52" N 108° 30' 14" W in Decimal Degrees, + it may appear as 45.7811111 -108.5038888 when represented numerically. + */ + static Latitude fromString(const std::string &coord); }; //! Implements the longitude - the measurement east or west of the prime meridian. @@ -81,6 +155,30 @@ class ERKIR_EXPORT Longitude : public Coordinate \throws std::out_of_range */ Longitude(double degree); + + std::string toString(Format format, int precision = 2) const override; + + /// Returns the longitude from the human readable coordinates (formatted). + /*! + Latitude/Longitude coordinates in three formats: Degrees Minutes Seconds (D° M' S"), + Decimal Minutes (D° M.M'), and Decimal Degrees (D.D°). Each of these formats + can represent the same geographic location, but expressed differently. + + For example: 45° 46' 52" N 108° 30' 14" W as displayed in Degrees Minutes Seconds (D° M' S"). + This same location, in displayed Decimal Minutes (D° M.M'), is: 45° 46.8666' N 108° 30.2333' W. + In Decimal Degrees (D.D°), this same location is: 45.7811111° N 108.5038888° W + + About Sign and North, South, East West + --------------------------------------------------------------------------- + Latitude/Longitude is followed by an indication of hemisphere. + For example 45° 46' 52" N indicates the Northern Hemisphere (North of the equator.) + 108° 30' 14" W indicates an area West of the Prime Meridian. When noting this numerically + (especially in Decimal Degrees), positive and negative values are sometimes used. + A positive value for North and East, a negative value for South and West. + Thus, in our example, when noting 45° 46' 52" N 108° 30' 14" W in Decimal Degrees, + it may appear as 45.7811111 -108.5038888 when represented numerically. + */ + static Longitude fromString(const std::string &coord); }; } diff --git a/src/coordinate.cpp b/src/coordinate.cpp index 05f8067..b64fce6 100644 --- a/src/coordinate.cpp +++ b/src/coordinate.cpp @@ -24,15 +24,158 @@ #define _USE_MATH_DEFINES +#include "coordinate.h" + +#include +#include +#include +#include #include +#include +#include #include +#include -#include "coordinate.h" +namespace erkir +{ + +template +struct FormatHash +{ + std::size_t operator()(const T &type) const + { + return static_cast(type); + } +}; + +using RxList = std::unordered_map>; constexpr double radiansInDegree = M_PI / 180.0; -namespace erkir +static const std::wstring degreeSigns = LR"([\u00B0\u00BA])"; +static const std::wstring minSigns = LR"(['\u2019\u2032\u02B9])"; +static const std::wstring secSigns = LR"(["\u201D\u2033])"; +static const std::wstring floatNumber = LR"([0-9]*[.,]?[0-9]*)"; + +static std::wstring dms(const std::wstring &directions) +{ + return { LR"(\s*([0-9]+)\s*)" + + degreeSigns + + LR"(\s*([0-5]?[0-9])\s*)" + minSigns + // Minutes [0, 60) + LR"(\s*()" + + floatNumber + + LR"()\s*)" + + secSigns + + LR"(\s*()" + + directions + + LR"()?\s*|\s*([0-9]+)\s+([0-5]?[0-9]+)\s+()" + + floatNumber + + LR"()\s+()" + + directions + + LR"()?\s*)" }; +} + +static std::wstring ddm(const std::wstring &directions) { + return { LR"(\s*([0-9]+)\s*)" + + degreeSigns + + LR"(\s*()" + floatNumber + LR"()\s*)" + minSigns + + LR"(\s*()" + + directions + + LR"()?\s*|\s*([0-9]+)\s+()" + + floatNumber + + LR"()\s+()" + + directions + + LR"()?\s*)" }; +} + +static std::wstring dd(const std::wstring &directions) +{ + return { LR"(\s*(-?)" + + floatNumber + + LR"()\s*)" + + degreeSigns + + LR"(?\s*()" + + directions + + LR"()?\s*)" }; +} + +static double parseRx(const std::string &coord, const RxList &rxList, char hemisphere) +{ + static std::wstring_convert> converter; + auto wcoord = converter.from_bytes(coord.c_str()); + + auto parseRx = [] (const std::wstring &ws, const std::wregex &rx) -> std::vector { + std::vector ret; + std::match_results match; + if (std::regex_match(ws, match, rx)) { + for (std::size_t i = 1; i < match.size(); ++i) { + const auto &sub_match = match[i]; + auto piece = sub_match.str(); + + if (!piece.empty()) { + ret.emplace_back(piece); + } + } + } + return ret; + }; + + auto toDouble = [] (const std::wstring &value, double max) { + static constexpr const double epsilon = 0.001; + const auto d = std::stof(value); + if (d - max > epsilon) { + throw std::runtime_error("Invalid or unsupported coordinates format"); + } + + return d; + }; + + double dd = 0.0; + + for (auto && rx : rxList) { + auto v = parseRx(wcoord, rx.second); + if (!v.empty()) { + switch (rx.first) { + case Coordinate::Format::DD: + { + assert(v.size() >= 1); + dd = std::stof(v.at(0)); + break; + }; + case Coordinate::Format::DDM: + { + assert(v.size() >= 2); + auto d = std::stof(v.at(0)); + auto m = toDouble(v.at(1), 60.0); + dd = d + m / 60.0; + break; + } + case Coordinate::Format::DMS: + { + assert(v.size() >= 3); + auto d = std::stof(v.at(0)); + auto m = toDouble(v.at(1), 60.0); + auto s = toDouble(v.at(2), 60.0); + dd = d + m / 60.0 + s / 3600.0; + break; + } + default: + assert(false); + break; + } + + if (std::toupper(v.back().at(0)) == hemisphere && dd > 0.0) { + dd = -dd; + } + + return dd; + } + } + + throw std::runtime_error("Invalid or unsupported coordinates format"); +} Coordinate::Coordinate(double degrees) : @@ -69,6 +212,67 @@ double Coordinate::wrap360(double degrees) return fmod(degrees + 360.0, 360.0); } +std::string Coordinate::toBaseString(Format format, int precision) const +{ + static const std::string degreeSign{ "°"}; + static const std::string minSign { "'" }; + static const std::string secSign { "\"" }; + + double degrees{}; + const auto min = std::abs(std::modf(m_degrees, °rees)) * 60.0; + + std::ostringstream ss; + ss.fill('0'); + + switch (format) { + case Format::DMS: + { + const auto sec = std::abs((min - int(min))) * 60.0; + + ss << std::setw(2) + << std::abs(int(degrees)) << degreeSign << ' ' + << std::setw(2) + << int(min) << minSign << ' ' + << std::setw(2) + << std::setprecision(precision + 2) + << sec << secSign; + break; + } + case Format::DDM: + ss << std::setw(2) + << std::abs(int(degrees)) << degreeSign << ' ' + << std::setw(2) + << std::setprecision(precision + 2) + << min << minSign; + break; + case Format::DD: + ss << std::setw(2) + << std::abs(m_degrees) << degreeSign; + break; + default: + break; + } + + return ss.str(); +} + +std::string Coordinate::compassPoint(double bearing, CompassPrecision precision) +{ + const auto wrappedBearing = wrap360(bearing); // normalize to range 0..360° + + static constexpr const char *cardinals[] = { + "N", "NNE", "NE", "ENE", + "E", "ESE", "SE", "SSE", + "S", "SSW", "SW", "WSW", + "W", "WNW", "NW", "NNW" + }; + static constexpr const int ns[] = { 4, 8, 16 }; + + const auto p = static_cast(precision); + const auto n = ns[p]; + return cardinals[(int)(std::round(wrappedBearing * n / 360.0)) % n * 16 / n]; +} + //////////////////////////////////////////////////////////////////////////////// Latitude::Latitude(double degree) @@ -81,6 +285,28 @@ Latitude::Latitude(double degree) } } +Latitude Latitude::fromString(const std::string &coord) +{ + static const std::wstring directions = LR"([NSns])"; + static const std::wregex rxDMS{ dms(directions) }; + static const std::wregex rxDDM{ ddm(directions) }; + static const std::wregex rxDD { dd (directions) }; + + static const RxList rxList = + { + { Coordinate::Format::DMS, rxDMS }, + { Coordinate::Format::DDM, rxDDM }, + { Coordinate::Format::DD , rxDD } + }; + + return { parseRx(coord, rxList, 'S')}; +} + +std::string Latitude::toString(Coordinate::Format format, int precision) const +{ + return toBaseString(format, precision) + ' ' + (degrees() < 0 ? 'S' : 'N'); +} + //////////////////////////////////////////////////////////////////////////////// Longitude::Longitude(double degree) @@ -93,5 +319,27 @@ Longitude::Longitude(double degree) } } +Longitude Longitude::fromString(const std::string &coord) +{ + static const std::wstring directions = LR"([EWew])"; + static const std::wregex rxDMS{ dms(directions) }; + static const std::wregex rxDDM{ ddm(directions) }; + static const std::wregex rxDD { dd (directions) }; + + static const RxList rxList = + { + { Coordinate::Format::DMS, rxDMS }, + { Coordinate::Format::DDM, rxDDM }, + { Coordinate::Format::DD , rxDD } + }; + + return { parseRx(coord, rxList, 'W')}; } +std::string Longitude::toString(Coordinate::Format format, int precision) const +{ + return toBaseString(format, precision) + ' ' + (degrees() < 0 ? 'W' : 'E'); +} + +} // namespace erkir + diff --git a/test/test.cpp b/test/test.cpp index 178e374..f756364 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -22,15 +22,16 @@ * SOFTWARE. * ***********************************************************************************/ -#include -#include -#include - #include "sphericalpoint.h" #include "ellipsoidalpoint.h" #include "cartesianpoint.h" #include "vector3d.h" +#include +#include +#include +#include + using namespace erkir; static int s_passed = 0; @@ -48,7 +49,7 @@ static std::string boolToString(bool value) static bool verifyDouble(double value, double expected, const std::string &location) { - static const double epsilon = 0.001; + static constexpr const double epsilon = 0.001; if (std::abs(value - expected) > epsilon) { fprintf(stderr, "%s VALUE ERROR: actual=%.3f, expected=%.3f\n", location.c_str(), value, expected); @@ -59,6 +60,18 @@ static bool verifyDouble(double value, double expected, const std::string &locat return true; } +static bool verifyString(const std::string &expected, const std::string &actual) +{ + if (expected != actual) { + fprintf(stderr, "%s VALUE ERROR: expected '%s' but got '%s'\n", LOCATION.c_str(), + expected.c_str(), actual.c_str()); + ++s_failed; + return false; + } + ++s_passed; + return true; +} + static bool verifyPoint(const Point &point, const Point &expectedPoint, const std::string &location) { @@ -211,6 +224,370 @@ int main() verifyDouble(Coordinate::wrap360(361.0), 1.0, LOCATION); } + // Coordinates from string + { + static const std::vector> lonVariants = + { + { "45.76260" , 45.7626 }, + { "-45.76260" ,-45.7626 }, + { " 45.76260 " , 45.7626}, + { "45.76260°" , 45.7626 }, + { "45.76260° W " ,-45.7626 }, + { "45 E" , 45.0000 }, + { ".76260" , 0.7630 }, + + { "45° 46.1' " , 45.7680 }, + { " 45° 46.1' E " , 45.7680 }, + { "45°46.756′E" , 45.7793 }, + { "45° 46.756′ E" , 45.7793 }, + { "45 46.756 W" ,-45.7793 }, + { "45 46 W" ,-45.7666 }, + { "45 .756 W" ,-45.0126 }, + { "45 59.9999 W" ,-45.9999 }, + + { "45°46′45.36″ " , 45.7790 }, + { "45°46′45.36″ E" , 45.7790 }, + { "45º46'47.36\" e" , 45.7790 }, + { "45º 46' 47.36\" e", 45.7790 }, + { "45°46’47.36” E" , 45.7790 }, + { "45 46 47.36 W" ,-45.7790 }, + { "45° 46′ 47.36″ e" , 45.7790 }, + { "45° 46’ 47.36” w" ,-45.7790 }, + { "45° 46’ 47” W" ,-45.7800 }, + { "45° 46’ .36” w" ,-45.7670 }, + }; + + static const std::vector> latVariants = + { + { "45.76260" , 45.7626 }, + { "-45.76260" ,-45.7626 }, + { " 45.76260 " , 45.7626}, + { "45.76260°" , 45.7626 }, + { "45.76260° N " , 45.7626 }, + { "45" , 45.0000 }, + { ".76260 S" , -0.7630 }, + + { "45° 46.1' " , 45.7680 }, + { " 45° 46.1' N " , 45.7680 }, + { "45°46.756′N" , 45.7793 }, + { "45° 46.756′ N" , 45.7793 }, + { "45 46.756 S" ,-45.7793 }, + { "45 46 S" ,-45.7666 }, + { "45 .756 S" ,-45.0126 }, + { "45 59.9999 S" ,-45.9999 }, + + { "45°46′45.36″ " , 45.7790 }, + { "45°46′45.36″ N" , 45.7790 }, + { "45º46'47.36\" S" ,-45.7790 }, + { "45º 46' 47.36\" n", 45.7790 }, + { "45°46’47.36” N" , 45.7790 }, + { "45 46 47.36 S" ,-45.7790 }, + { "45° 46′ 47.36″ n" , 45.7790 }, + { "45° 46’ 47.36” s" ,-45.7790 }, + { "45° 46’ 47” S" ,-45.7800 }, + { "45° 46’ .36” s" ,-45.7670 }, + }; + + for (auto && v : lonVariants) { + auto lon = Longitude::fromString(v.first); + verifyDouble(lon.degrees(), v.second, LOCATION); + } + + for (auto &&v : latVariants) { + auto lat = Latitude::fromString(v.first); + verifyDouble(lat.degrees(), v.second, LOCATION); + } + + ///////////////////////////////////////////////////////////////////////// + + static const std::vector lonError = { + "", + "180.1", + "180.76260° W ", + "45° 60’ .36” w", + "180° 46’ .36” w", + "45° 46’ 60.36” w" + }; + + for (auto && v : lonError) { + try { + Longitude::fromString(v); + fprintf(stderr, "%s VALUE ERROR: expected exception\n", LOCATION .c_str()); + ++s_failed; + } + catch (const std::exception &) { + ++s_passed; + } + } + + static const std::vector latError = { + "", + "90.1", + "90.76260° W ", + "45° 60’ .36” w", + "90° 46’ .36” w", + "45° 46’ 60.36” w" + }; + + for (auto &&v : latError) { + try { + Latitude::fromString(v); + fprintf(stderr, "%s VALUE ERROR: expected exception\n", LOCATION.c_str()); + ++s_failed; + } + catch (const std::exception &) { + ++s_passed; + } + } + } + + // Coordinates to string DMS + { + static const std::vector> lonVariants = + { + { "45.76260" , "45° 45' 45.36\" E"}, + { "-45.76260" , "45° 45' 45.36\" W" }, + { " 45.76260 " , "45° 45' 45.36\" E"}, + { "45.76260°" , "45° 45' 45.36\" E" }, + { "45.76260° W " , "45° 45' 45.36\" W" }, + { "45 E" , "45° 00' 00\" E" }, + { ".76260" , "00° 45' 45.36\" E" }, + + { "45° 46.1' " , "45° 46' 06\" E" }, + { " 45° 46.1' E " , "45° 46' 06\" E" }, + { "45°46.756′E" , "45° 46' 45.36\" E" }, + { "45° 46.756′ E" , "45° 46' 45.36\" E" }, + { "45 46.756 W" , "45° 46' 45.36\" W" }, + { "45 46 W" , "45° 45' 60\" W" }, + { "45 .756 W" , "45° 00' 45.36\" W" }, + { "45 59.9999 W" , "45° 59' 59.99\" W" }, + + { "45°46′45.36″ " , "45° 46' 45.36\" E" }, + { "45°46′45.36″ E" , "45° 46' 45.36\" E" }, + { "45º46'47.36\" e" , "45° 46' 47.36\" E" }, + { "45º 46' 47.36\" e", "45° 46' 47.36\" E" }, + { "45°46’47.36” E" , "45° 46' 47.36\" E" }, + { "45 46 47.36 W" , "45° 46' 47.36\" W" }, + { "45° 46′ 47.36″ e" , "45° 46' 47.36\" E" }, + { "45° 46’ 47.36” w" , "45° 46' 47.36\" W" }, + { "45° 46’ 47” W" , "45° 46' 47\" W" }, + { "45° 46’ .36” w" , "45° 46' 0.36\" W" }, + }; + + static const std::vector> latVariants = + { + { "45.76260" , "45° 45' 45.36\" N" }, + { "-45.76260" , "45° 45' 45.36\" S" }, + { " 45.76260 " , "45° 45' 45.36\" N"}, + { "45.76260°" , "45° 45' 45.36\" N" }, + { "45.76260° N " , "45° 45' 45.36\" N" }, + { "45" , "45° 00' 00\" N"}, + { ".76260 S" , "00° 45' 45.36\" S" }, + + { "45° 46.1' " , "45° 46' 06\" N" }, + { " 45° 46.1' N " , "45° 46' 06\" N" }, + { "45°46.756′N" , "45° 46' 45.36\" N" }, + { "45° 46.756′ N" , "45° 46' 45.36\" N" }, + { "45 46.756 S" , "45° 46' 45.36\" S" }, + { "45 46 S" , "45° 45' 60\" S" }, + { "45 .756 S" , "45° 00' 45.36\" S" }, + { "45 59.9999 S" , "45° 59' 59.99\" S" }, + + { "45°46′45.36″ " , "45° 46' 45.36\" N" }, + { "45°46′45.36″ N" , "45° 46' 45.36\" N" }, + { "45º46'47.36\" S" , "45° 46' 47.36\" S" }, + { "45º 46' 47.36\" n", "45° 46' 47.36\" N" }, + { "45°46’47.36” N" , "45° 46' 47.36\" N" }, + { "45 46 47.36 S" , "45° 46' 47.36\" S" }, + { "45° 46′ 47.36″ n" , "45° 46' 47.36\" N" }, + { "45° 46’ 47.36” s" , "45° 46' 47.36\" S" }, + { "45° 46’ 47” S" , "45° 46' 47\" S" }, + { "45° 46’ .36” s" , "45° 46' 0.36\" S" }, + }; + + for (auto && v : lonVariants) { + const auto lon = Longitude::fromString(v.first); + verifyString(v.second, lon.toString(Coordinate::Format::DMS)); + } + + for (auto && v : latVariants) { + const auto lat = Latitude::fromString(v.first); + verifyString(v.second, lat.toString(Coordinate::Format::DMS)); + } + } + + // Coordinates to string DDM + { + static const std::vector> lonVariants = + { + { "45.76260" , "45° 45.756' E"}, + { "-45.76260" , "45° 45.756' W" }, + { " 45.76260 " , "45° 45.756' E"}, + { "45.76260°" , "45° 45.756' E" }, + { "45.76260° W " , "45° 45.756' W" }, + { "45 E" , "45° 00' E" }, + { ".76260" , "00° 45.756' E" }, + + { "45° 46.1' " , "45° 46.1' E" }, + { " 45° 46.1' E " , "45° 46.1' E" }, + { "45°46.756′E" , "45° 46.756' E" }, + { "45° 46.756′ E" , "45° 46.756' E" }, + { "45 46.756 W" , "45° 46.756' W" }, + { "45 46 W" , "45° 46' W" }, + { "45 .756 W" , "45° 0.756' W" }, + { "45 59.9999 W" , "45° 60' W" }, + + { "45°46′45.36″ " , "45° 46.756' E" }, + { "45°46′45.36″ E" , "45° 46.756' E" }, + { "45º46'47.36\" e" , "45° 46.789' E" }, + { "45º 46' 47.36\" e", "45° 46.789' E" }, + { "45°46’47.36” E" , "45° 46.789' E" }, + { "45 46 47.36 W" , "45° 46.789' W" }, + { "45° 46′ 47.36″ e" , "45° 46.789' E" }, + { "45° 46’ 47.36” w" , "45° 46.789' W" }, + { "45° 46’ 47” W" , "45° 46.783' W" }, + { "45° 46’ .36” w" , "45° 46.006' W" }, + }; + + static const std::vector> latVariants = + { + { "45.76260" , "45° 45.756' N" }, + { "-45.76260" , "45° 45.756' S" }, + { " 45.76260 " , "45° 45.756' N"}, + { "45.76260°" , "45° 45.756' N" }, + { "45.76260° N " , "45° 45.756' N" }, + { "45" , "45° 00' N" }, + { ".76260 S" , "00° 45.756' S" }, + + { "45° 46.1' " , "45° 46.1' N" }, + { " 45° 46.1' N " , "45° 46.1' N" }, + { "45°46.756′N" , "45° 46.756' N" }, + { "45° 46.756′ N" , "45° 46.756' N" }, + { "45 46.756 S" , "45° 46.756' S" }, + { "45 46 S" , "45° 46' S" }, + { "45 .756 S" , "45° 0.756' S" }, + { "45 59.9999 S" , "45° 60' S" }, + + { "45°46′45.36″ " , "45° 46.756' N" }, + { "45°46′45.36″ N" , "45° 46.756' N" }, + { "45º46'47.36\" S" , "45° 46.789' S" }, + { "45º 46' 47.36\" n", "45° 46.789' N" }, + { "45°46’47.36” N" , "45° 46.789' N" }, + { "45 46 47.36 S" , "45° 46.789' S" }, + { "45° 46′ 47.36″ n" , "45° 46.789' N" }, + { "45° 46’ 47.36” s" , "45° 46.789' S" }, + { "45° 46’ 47” S" , "45° 46.783' S" }, + { "45° 46’ .36” s" , "45° 46.006' S" }, + }; + + for (auto &&v : lonVariants) { + const auto lon = Longitude::fromString(v.first); + verifyString(v.second, lon.toString(Coordinate::Format::DDM, 3)); + } + + for (auto &&v : latVariants) { + const auto lat = Latitude::fromString(v.first); + verifyString(v.second, lat.toString(Coordinate::Format::DDM, 3)); + } + } + + // Coordinates to string DD + { + static const std::vector> lonVariants = + { + { "45.76260" , "45.7626° E" }, + { "-45.76260" , "45.7626° W" }, + { " 45.76260 " , "45.7626° E" }, + { "45.76260°" , "45.7626° E" }, + { "45.76260° W " , "45.7626° W" }, + { "45 E" , "45° E" }, + { ".76260" , "0.7626° E" }, + + { "45° 46.1' " , "45.7683° E" }, + { " 45° 46.1' E " , "45.7683° E" }, + { "45°46.756′E" , "45.7793° E" }, + { "45° 46.756′ E" , "45.7793° E" }, + { "45 46.756 W" , "45.7793° W" }, + { "45 46 W" , "45.7667° W" }, + { "45 .756 W" , "45.0126° W" }, + { "45 59.9999 W" , "46° W" }, + + { "45°46′45.36″ " , "45.7793° E" }, + { "45°46′45.36″ E" , "45.7793° E" }, + { "45º46'47.36\" e" , "45.7798° E" }, + { "45º 46' 47.36\" e", "45.7798° E" }, + { "45°46’47.36” E" , "45.7798° E" }, + { "45 46 47.36 W" , "45.7798° W" }, + { "45° 46′ 47.36″ e" , "45.7798° E" }, + { "45° 46’ 47.36” w" , "45.7798° W" }, + { "45° 46’ 47” W" , "45.7797° W" }, + { "45° 46’ .36” w" , "45.7668° W" }, + }; + + static const std::vector> latVariants = + { + { "45.76260" , "45.7626° N" }, + { "-45.76260" , "45.7626° S" }, + { " 45.76260 " , "45.7626° N" }, + { "45.76260°" , "45.7626° N" }, + { "45.76260° N " , "45.7626° N" }, + { "45" , "45° N" }, + { ".76260 S" , "0.7626° S" }, + + { "45° 46.1' " , "45.7683° N" }, + { " 45° 46.1' N " , "45.7683° N" }, + { "45°46.756′N" , "45.7793° N" }, + { "45° 46.756′ N" , "45.7793° N" }, + { "45 46.756 S" , "45.7793° S" }, + { "45 46 S" , "45.7667° S" }, + { "45 .756 S" , "45.0126° S" }, + { "45 59.9999 S" , "46° S" }, + + { "45°46′45.36″ " , "45.7793° N" }, + { "45°46′45.36″ N" , "45.7793° N" }, + { "45º46'47.36\" S" , "45.7798° S" }, + { "45º 46' 47.36\" n", "45.7798° N" }, + { "45°46’47.36” N" , "45.7798° N" }, + { "45 46 47.36 S" , "45.7798° S" }, + { "45° 46′ 47.36″ n" , "45.7798° N" }, + { "45° 46’ 47.36” s" , "45.7798° S" }, + { "45° 46’ 47” S" , "45.7797° S" }, + { "45° 46’ .36” s" , "45.7668° S" }, + }; + + for (auto &&v : lonVariants) { + const auto lon = Longitude::fromString(v.first); + verifyString(v.second, lon.toString(Coordinate::Format::DD, 3)); + } + + for (auto &&v : latVariants) { + const auto lat = Latitude::fromString(v.first); + verifyString(v.second, lat.toString(Coordinate::Format::DD, 3)); + } + } + + // Compass Points + { + verifyString( "N", Coordinate::compassPoint(1)); + verifyString( "N", Coordinate::compassPoint(720)); + verifyString( "N", Coordinate::compassPoint(0)); + verifyString( "N", Coordinate::compassPoint(-1)); + verifyString( "N", Coordinate::compassPoint(359)); + verifyString( "S", Coordinate::compassPoint(180)); + verifyString("NNE", Coordinate::compassPoint(24)); + verifyString( "N", Coordinate::compassPoint(24, Coordinate::CompassPrecision::Cardinal)); + verifyString( "NE", Coordinate::compassPoint(24, Coordinate::CompassPrecision::Intercardinal)); + verifyString("NNE", Coordinate::compassPoint(24, Coordinate::CompassPrecision::SecondaryIntercardinal)); + verifyString( "SW", Coordinate::compassPoint(226)); + verifyString( "W", Coordinate::compassPoint(226, Coordinate::CompassPrecision::Cardinal)); + verifyString( "SW", Coordinate::compassPoint(226, Coordinate::CompassPrecision::Intercardinal)); + verifyString( "SW", Coordinate::compassPoint(226, Coordinate::CompassPrecision::SecondaryIntercardinal)); + verifyString("WSW", Coordinate::compassPoint(237)); + verifyString( "W", Coordinate::compassPoint(237, Coordinate::CompassPrecision::Cardinal)); + verifyString( "SW", Coordinate::compassPoint(237, Coordinate::CompassPrecision::Intercardinal)); + verifyString("WSW", Coordinate::compassPoint(237, Coordinate::CompassPrecision::SecondaryIntercardinal)); + } + ////////////////////////////////////////////////////////////////////////////// // Ellipsoidal points