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) 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 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(); } +