From 861be5c11a271b3f66e72eb3d7decb7a6c6c1880 Mon Sep 17 00:00:00 2001 From: Vahan Aghajanyan Date: Thu, 18 Feb 2021 13:50:15 +0100 Subject: [PATCH 1/3] Make coordinates precision configurable. The PolylineEncoder class used to use a fixed decimal places precision (5 digits) and this conformed with Google's utility which rounded all values up to the fifth digit. This change makes the number of digits configurable. The PolylineEncoder is now a generic class whose template parameter defines the number of decimal places it should use (defaulted to five). --- src/polylineencoder.cpp | 47 +++++++++++++++++++++++++---------------- src/polylineencoder.h | 33 +++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/polylineencoder.cpp b/src/polylineencoder.cpp index 4e1f7c5..591f901 100644 --- a/src/polylineencoder.cpp +++ b/src/polylineencoder.cpp @@ -29,7 +29,6 @@ #include "polylineencoder.h" -static const double s_presision = 100000.0; static const int s_chunkSize = 5; static const int s_asciiOffset = 63; static const int s_5bitMask = 0x1f; // 0b11111 = 31 @@ -38,37 +37,44 @@ static const int s_6bitMask = 0x20; // 0b100000 = 32 namespace gepaf { -PolylineEncoder::Point::Point(double latitude, double longitude) - : m_latitude(std::round(latitude * s_presision) / s_presision) - , m_longitude(std::round(longitude * s_presision) / s_presision) +template +PolylineEncoder::Point::Point(double latitude, double longitude) + : m_latitude (std::round(latitude * Precision::Value) / Precision::Value) + , m_longitude(std::round(longitude * Precision::Value) / Precision::Value) { assert(latitude <= 90.0 && latitude >= -90.0); assert(longitude <= 180.0 && longitude >= -180.0); } -double PolylineEncoder::Point::latitude() const +template +double PolylineEncoder::Point::latitude() const { return m_latitude; } -double PolylineEncoder::Point::longitude() const +template +double PolylineEncoder::Point::longitude() const { return m_longitude; } -void PolylineEncoder::addPoint(double latitude, double longitude) +template +void PolylineEncoder::addPoint(double latitude, double longitude) { m_polyline.emplace_back(latitude, longitude); } -std::string PolylineEncoder::encode() const +template +std::string PolylineEncoder::encode() const { return encode(m_polyline); } -std::string PolylineEncoder::encode(double value) +template +std::string PolylineEncoder::encode(double value) { - int32_t e5 = std::round(value * s_presision); // (2) + int32_t e5 = + std::round(value * Precision::Value); // (2) e5 <<= 1; // (4) @@ -97,7 +103,8 @@ std::string PolylineEncoder::encode(double value) return result; } -std::string PolylineEncoder::encode(const PolylineEncoder::Polyline &polyline) +template +std::string PolylineEncoder::encode(const PolylineEncoder::Polyline &polyline) { std::string result; @@ -121,7 +128,8 @@ std::string PolylineEncoder::encode(const PolylineEncoder::Polyline &polyline) return result; } -double PolylineEncoder::decode(const std::string &coords, size_t &i) +template +double PolylineEncoder::decode(const std::string &coords, size_t &i) { assert(i < coords.size()); @@ -144,13 +152,14 @@ double PolylineEncoder::decode(const std::string &coords, size_t &i) } result >>= 1; // (4) - // Convert to decimal value. - return result / s_presision; // (2) + // Convert to decimal value with the given precision. + return result / static_cast(Precision::Value); // (2) } -PolylineEncoder::Polyline PolylineEncoder::decode(const std::string &coords) +template +typename PolylineEncoder::Polyline PolylineEncoder::decode(const std::string &coords) { - PolylineEncoder::Polyline polyline; + PolylineEncoder::Polyline polyline; size_t i = 0; while (i < coords.size()) @@ -183,12 +192,14 @@ PolylineEncoder::Polyline PolylineEncoder::decode(const std::string &coords) return polyline; } -const PolylineEncoder::Polyline &PolylineEncoder::polyline() const +template +const typename PolylineEncoder::Polyline &PolylineEncoder::polyline() const { return m_polyline; } -void PolylineEncoder::clear() +template +void PolylineEncoder::clear() { m_polyline.clear(); } diff --git a/src/polylineencoder.h b/src/polylineencoder.h index eae9fc0..5e400ae 100644 --- a/src/polylineencoder.h +++ b/src/polylineencoder.h @@ -36,9 +36,11 @@ namespace gepaf For more details refer to the algorithm definition at https://developers.google.com/maps/documentation/utilities/polylinealgorithm - The implementation guarantees to conform with the results of the Google Interactive - Polyline Encoder Utility (https://developers.google.com/maps/documentation/utilities/polylineutility) + Default implementation (precision of 5 decimal places) guarantees to conform + with the results of the Google Interactive Polyline Encoder Utility + (https://developers.google.com/maps/documentation/utilities/polylineutility) */ +template class PolylineEncoder { public: @@ -49,9 +51,9 @@ class PolylineEncoder /// Creates a geodetic point with the given coordinates. /*! Both latitude and longitude will be rounded to a reasonable precision - of 5 decimal places. - \param latitude The latitude in decimal point degrees. The values are bounded by ±90.0°. - \param longitude The longitude in decimal point degrees. The values are bounded by ±180.0°. + of 5 decimal places (default) or to the number of digits specified by the template parameter.. + \param latitude The latitude in decimal point degrees. The values are bounded by ±90.0°. + \param longitude The longitude in decimal point degrees. The values are bounded by ±180.0°. */ Point(double latitude, double longitude); @@ -72,7 +74,10 @@ class PolylineEncoder //! Adds new point with the given \p latitude and \p longitude for encoding. /*! Note: both latitude and longitude will be rounded to a reasonable precision - of 5 decimal places. + of 5 decimal places (default) or to the number of digits specified by the + template parameter. + \param latitude The latitude in decimal point degrees. The values are bounded by ±90.0°. + \param longitude The longitude in decimal point degrees. The values are bounded by ±180.0°. */ void addPoint(double latitude, double longitude); @@ -94,6 +99,11 @@ class PolylineEncoder //! Returns polyline decoded from the given \p coordinates string. static Polyline decode(const std::string &coordinates); + enum Precision + { + Value = PolylineEncoder::Precision::Value * 10 + }; + private: //! Encodes a single value according to the compression algorithm. static std::string encode(double value); @@ -105,6 +115,17 @@ class PolylineEncoder Polyline m_polyline; }; +// A bogus class for compile-time precision calculations. +template<> +class PolylineEncoder<0> +{ +public: + enum Precision + { + Value = 1 // 10^0 = 1 + }; +}; + } // namespace #endif // POLYLINEENCODER_H From f2a0ddcfb6e77c55c6107bdae3fe6011b5fa5f66 Mon Sep 17 00:00:00 2001 From: Vahan Aghajanyan Date: Thu, 18 Feb 2021 13:55:24 +0100 Subject: [PATCH 2/3] Update and extend the unit test according to the main class changes. --- test/main.cpp | 103 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/test/main.cpp b/test/main.cpp index 5bbca7d..cd68204 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -26,23 +26,24 @@ SOFTWARE. #include -static bool operator==(const gepaf::PolylineEncoder::Point& l, const gepaf::PolylineEncoder::Point& r) +template +bool operator==(const Point &l, const Point &r) { EXPECT_DOUBLE_EQ(l.longitude(), r.longitude()); - EXPECT_DOUBLE_EQ(l.latitude(), r.latitude()); + EXPECT_DOUBLE_EQ(l.latitude(), r.latitude()); return true; } TEST(General, ZeroPoint) { - gepaf::PolylineEncoder encoder; + gepaf::PolylineEncoder<> encoder; encoder.addPoint(.0, .0); EXPECT_EQ(encoder.encode(), "??"); } TEST(General, PolesAndEquator) { - gepaf::PolylineEncoder encoder; + gepaf::PolylineEncoder<> encoder; // Poles and equator. encoder.addPoint(-90.0, -180.0); @@ -54,7 +55,7 @@ TEST(General, PolesAndEquator) TEST(General, EmptyList) { // Empty list of points. - gepaf::PolylineEncoder encoder; + gepaf::PolylineEncoder<> encoder; EXPECT_EQ(encoder.encode(), std::string()); } @@ -62,7 +63,7 @@ TEST(General, EmptyList) TEST(General, StandardExample) { // Coordinates from https://developers.google.com/maps/documentation/utilities/polylinealgorithm - gepaf::PolylineEncoder encoder; + gepaf::PolylineEncoder<> encoder; encoder.addPoint(38.5, -120.2); encoder.addPoint(40.7, -120.95); encoder.addPoint(43.252, -126.453); @@ -79,41 +80,83 @@ TEST(General, StandardExample) TEST(General, BasicDecode) { // Decode a valid polyline string. - auto decodedPolyline = gepaf::PolylineEncoder::decode("_p~iF~ps|U_ulLnnqC_mqNvxq`@"); + auto decodedPolyline = gepaf::PolylineEncoder<>::decode("_p~iF~ps|U_ulLnnqC_mqNvxq`@"); EXPECT_EQ(decodedPolyline.size(), 3); - EXPECT_TRUE(decodedPolyline[0] == gepaf::PolylineEncoder::Point(38.5, -120.2)); - EXPECT_TRUE(decodedPolyline[1] == gepaf::PolylineEncoder::Point(40.7, -120.95)); - EXPECT_TRUE(decodedPolyline[2] == gepaf::PolylineEncoder::Point(43.252, -126.453)); + EXPECT_TRUE(decodedPolyline[0] == gepaf::PolylineEncoder<>::Point(38.5, -120.2)); + EXPECT_TRUE(decodedPolyline[1] == gepaf::PolylineEncoder<>::Point(40.7, -120.95)); + EXPECT_TRUE(decodedPolyline[2] == gepaf::PolylineEncoder<>::Point(43.252, -126.453)); decodedPolyline.clear(); EXPECT_EQ(decodedPolyline.size(), 0); } +TEST(General, HighPrecision_6_digits) +{ + { + auto decodedPolyline = gepaf::PolylineEncoder<6>::decode("AA@@"); + EXPECT_EQ(decodedPolyline.size(), 2); + EXPECT_TRUE(decodedPolyline[0] == gepaf::PolylineEncoder<6>::Point(0.0000005, 0.0000005)); + EXPECT_TRUE(decodedPolyline[1] == gepaf::PolylineEncoder<6>::Point(0.0, 0.0)); + } + { + gepaf::PolylineEncoder<6> encoder; + encoder.addPoint(0.0000005, 0.0000005); + encoder.addPoint(0.0000000, 0.0000000); + EXPECT_EQ(encoder.encode(), "AA@@"); + } + { + gepaf::PolylineEncoder<6> encoder; + encoder.addPoint(47.231174468994141, 16.62629508972168); + encoder.addPoint(47.231208801269531, 16.626440048217773); + EXPECT_EQ(encoder.encode(), "kkwayAmfxu^eAaH"); + + auto decodedPolyline = gepaf::PolylineEncoder<6>::decode("kkwayAmfxu^eAaH"); + EXPECT_EQ(decodedPolyline.size(), 2); + EXPECT_TRUE(decodedPolyline[0] == gepaf::PolylineEncoder<6>::Point(47.231174, 16.626295)); + EXPECT_TRUE(decodedPolyline[1] == gepaf::PolylineEncoder<6>::Point(47.231209, 16.626440)); + } +} + +TEST(General, LowPrecision_1_digit) +{ + { + gepaf::PolylineEncoder<1> encoder; + encoder.addPoint(47.231174468994141, 16.54629508972168); + encoder.addPoint(47.335208801269531, 16.65440048217773); + EXPECT_EQ(encoder.encode(), "o\\iIAC"); + + auto decodedPolyline = gepaf::PolylineEncoder<1>::decode("o\\iIAC"); + EXPECT_EQ(decodedPolyline.size(), 2); + EXPECT_TRUE(decodedPolyline[0] == gepaf::PolylineEncoder<1>::Point(47.2, 16.5)); + EXPECT_TRUE(decodedPolyline[1] == gepaf::PolylineEncoder<1>::Point(47.3, 16.65)); + } +} + TEST(General, InvalidInputString1) { // String too short, last byte missing makes last coordinate invalid. - auto decodedPolyline = gepaf::PolylineEncoder::decode("_p~iF~ps|U_ulLnnqC_mqNvxq`"); + auto decodedPolyline = gepaf::PolylineEncoder<>::decode("_p~iF~ps|U_ulLnnqC_mqNvxq`"); EXPECT_EQ(decodedPolyline.size(), 0); } TEST(General, InvalidInputString2) { // String too short, last bytes missing makes last coordinate.lon missing. - auto decodedPolyline = gepaf::PolylineEncoder::decode("_p~iF~ps|U_ulLnnqC_mqN"); + auto decodedPolyline = gepaf::PolylineEncoder<>::decode("_p~iF~ps|U_ulLnnqC_mqN"); EXPECT_EQ(decodedPolyline.size(), 0); } TEST(General, InvalidInputString3) { // String too short, last coordinate.lon missing and last coordinate.lat invalid. - auto decodedPolyline = gepaf::PolylineEncoder::decode("_p~iF~ps|U_ulLnnqC_mq"); + auto decodedPolyline = gepaf::PolylineEncoder<>::decode("_p~iF~ps|U_ulLnnqC_mq"); EXPECT_EQ(decodedPolyline.size(), 0); } TEST(General, InvalidInputString4) { // Third byte changed from '~' to ' ', generating an invalid fourth coordinate. - auto decodedPolyline = gepaf::PolylineEncoder::decode("_p iF~ps|U_ulLnnqC_mqNvxq`@"); + auto decodedPolyline = gepaf::PolylineEncoder<>::decode("_p iF~ps|U_ulLnnqC_mqNvxq`@"); EXPECT_EQ(decodedPolyline.size(), 0); } @@ -121,7 +164,7 @@ TEST(General, InvalidInputString5) { // Fifth byte changed from 'F' to 'f' changing the 'next byte' flag in it, // leading to an extremely large latitude for the first coordinate. - auto decodedPolyline = gepaf::PolylineEncoder::decode("_p~if~ps|U_ulLnnqC_mqNvxq`@"); + auto decodedPolyline = gepaf::PolylineEncoder<>::decode("_p~if~ps|U_ulLnnqC_mqNvxq`@"); EXPECT_EQ(decodedPolyline.size(), 0); } @@ -129,32 +172,43 @@ TEST(General, InvalidInputString6) { // Tenth byte changed from 'U' to 'u' changing the 'next byte' flag in it, // leading to an extremely large longitude for the first coordinate. - auto decodedPolyline = gepaf::PolylineEncoder::decode("_p~iF~ps|u_ulLnnqC_mqNvxq`@"); + auto decodedPolyline = gepaf::PolylineEncoder<>::decode("_p~iF~ps|u_ulLnnqC_mqNvxq`@"); EXPECT_EQ(decodedPolyline.size(), 0); } TEST(General, DecodeEmptyString) { // Empty string. - auto decodedPolyline = gepaf::PolylineEncoder::decode(""); + auto decodedPolyline = gepaf::PolylineEncoder<>::decode(""); EXPECT_EQ(decodedPolyline.size(), 0); } TEST(General, PrecisionTest) { - // Avoid cumulated error - gepaf::PolylineEncoder encoder; - encoder.addPoint(0.0000005, 0.0000005); - encoder.addPoint(0.0000000, 0.0000000); + { + // Avoid cumulated error + gepaf::PolylineEncoder<> encoder; + encoder.addPoint(0.0000005, 0.0000005); + encoder.addPoint(0.0000000, 0.0000000); - // Expectation comes from https://developers.google.com/maps/documentation/utilities/polylineutility - EXPECT_EQ(encoder.encode(), "????"); + // Expectation comes from https://developers.google.com/maps/documentation/utilities/polylineutility + EXPECT_EQ(encoder.encode(), "????"); + } + { + // Avoid cumulated error + gepaf::PolylineEncoder<6> encoder; + encoder.addPoint(0.00000005, 0.00000005); + encoder.addPoint(0.00000000, 0.00000000); + + // Should be the same as with 5 digits precision. + EXPECT_EQ(encoder.encode(), "????"); + } } TEST(General, PrecisionTest2) { // Avoid cumulated error - gepaf::PolylineEncoder encoder; + gepaf::PolylineEncoder<> encoder; encoder.addPoint(47.231174468994141, 16.62629508972168); encoder.addPoint(47.231208801269531, 16.626440048217773); @@ -167,3 +221,4 @@ int main(int argc, char** argv) testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } + From 29f6a7bc7fd67ca0c5dabb11a28858fde3acff0a Mon Sep 17 00:00:00 2001 From: Vahan Aghajanyan Date: Thu, 18 Feb 2021 14:01:56 +0100 Subject: [PATCH 3/3] Update README file according to the changes. --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1f8c50d..e4808c2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The implementation guarantees to conform with the results of the [Google Interac ## Installation -No installation required. Just compile *polylineencoder.h(.cpp)* in your project and use `gepaf::PolylineEncoder` class. +No installation required. Just compile *polylineencoder.h(.cpp)* in your project and use `gepaf::PolylineEncoder` class template. ## Prerequisites @@ -20,7 +20,10 @@ For more details see the CI badges (*Travis CI & AppVeyor CI*). All code is in `gepaf` namespace. `gepaf` stands for *Google Encoded Polyline Algorithm Format*. ```cpp -gepaf::PolylineEncoder encoder; +// Create an encoder with precision of 5 decimal places (default) +// In order to create objects with other precision use template parameter +// like: gepaf::PolylineEncoder<6> +gepaf::PolylineEncoder<> encoder; // Poles and equator. encoder.addPoint(-90.0, -180.0); @@ -31,7 +34,7 @@ auto res = encoder.encode(); // "~bidP~fsia@_cidP_gsia@_cidP_gsia@" encoder.clear(); // Clear the list of points. // Decode a string using static function. -auto polyline = gepaf::PolylineEncoder::decode("~bidP~fsia@_cidP_gsia@_cidP_gsia@"); +auto polyline = gepaf::PolylineEncoder<>::decode("~bidP~fsia@_cidP_gsia@_cidP_gsia@"); // Iterate over all points and print coordinates of each. for (const auto &point : polyline) { @@ -41,7 +44,7 @@ for (const auto &point : polyline) { ## Building and Testing -There are unit tests provided for `PolylineEncoder` class. You can find them in the *test/* directory. +There are unit tests provided for `PolylineEncoder` class template. You can find them in the *test/* directory. To run them you have to build and run the test application (linking with Google Test library is required). For doing that you can invoke the following commands from the terminal, assuming that compiler and environment are already configured: ##### Linux (gcc)