diff --git a/R/README.md b/R/README.md deleted file mode 100644 index 00c8d57..0000000 --- a/R/README.md +++ /dev/null @@ -1,96 +0,0 @@ - -# Flexible Polyline Encoding for R - - -[![CRAN status](https://www.r-pkg.org/badges/version/flexpolyline)](https://CRAN.R-project.org/package=flexpolyline) -[![CRAN checks](https://cranchecks.info/badges/worst/flexpolyline)](https://cran.r-project.org/web/checks/check_results_flexpolyline.html) -[![CRAN downloads](https://cranlogs.r-pkg.org/badges/last-month/flexpolyline?color=brightgreen)](https://CRAN.R-project.org/package=flexpolyline) -[![Codecov test coverage](https://codecov.io/gh/munterfinger/flexpolyline/branch/master/graph/badge.svg)](https://codecov.io/gh/munterfinger/flexpolyline?branch=master) - - -The **[flexpolyline](https://CRAN.R-project.org/package=flexpolyline)** R package -binds to the [C++ implementation](https://github.com/heremaps/flexible-polyline/tree/master/cpp) -of the flexible polyline encoding by HERE. The package is designed to interface -with simple features of the **[sf](https://CRAN.R-project.org/package=sf)** package, -which is a common way of handling spatial data in R. For detailed information on -encoding and decoding polylines in R, see the package -[documentation](https://munterfinger.github.io/flexpolyline/index.html) -or the [repository](https://github.com/munterfinger/flexpolyline) on GitHub. - -**Note:** -* The order of the coordinates (lng, lat) does not correspond to the original -C++ implementation (lat, lng). This enables direct conversion to simple feature -objects without reordering the columns. -* Decoding gives reliable results up to a precision of 7 digits. -The package tests are also limited to this range. - -## Get started - -Install the released version of **flexpolyline** from CRAN: - -``` r -install.packages("flexpolyline") -``` - -Encoding and decoding in R is straightforward by using `encode()` and `decode()`. -These functions are binding to the flexpolyline C++ implementation and reflect -the arguments and return values of their counterparts (`hf::encode_polyline` and -`hf::decode_polyline`): - -``` r -library(flexpolyline) - -line <- matrix( - c(8.69821, 50.10228, 10, - 8.69567, 50.10201, 20, - 8.69150, 50.10063, 30, - 8.68752, 50.09878, 40), - ncol = 3, byrow = TRUE -) - -encode(line) -#> [1] "B1Voz5xJ67i1Bgkh9B1B7Pgkh9BzIhagkh9BxL7Ygkh9B" - -decode("B1Voz5xJ67i1Bgkh9B1B7Pgkh9BzIhagkh9BxL7Ygkh9B") -#> LNG LAT ELEVATION -#> [1,] 8.69821 50.10228 10 -#> [2,] 8.69567 50.10201 20 -#> [3,] 8.69150 50.10063 30 -#> [4,] 8.68752 50.09878 40 -``` - -A common way to deal with spatial data in R is the **sf** package, which is -built on the concept of simple features. The functions `encode_sf()` and -`decode_sf()` provide an interface that support the encoding of sf objects with -geometry type `LINESTRING`: - -``` r -sfg <- sf::st_linestring(line, dim = "XYZ") -print(sfg) -#> LINESTRING Z (8.69821 50.10228 10, 8.69567 50.10201 20, 8.6915 50.10063 3... - -encode_sf(sfg) -#> [1] "B1Voz5xJ67i1Bgkh9B1B7Pgkh9BzIhagkh9BxL7Ygkh9B" - -decode_sf("B1Voz5xJ67i1Bgkh9B1B7Pgkh9BzIhagkh9BxL7Ygkh9B", crs = 4326) -#> Simple feature collection with 1 feature and 2 fields -#> geometry type: LINESTRING -#> dimension: XYZ -#> bbox: xmin: 8.68752 ymin: 50.09878 xmax: 8.69821 ymax: 50.10228 -#> z_range: zmin: 10 zmax: 40 -#> geographic CRS: WGS 84 -#> id dim3 geometry -#> 1 1 ELEVATION LINESTRING Z (8.69821 50.10... -``` - -## References - -* [Flexible Polyline Encoding](https://github.com/heremaps/flexible-polyline) -* [flexpolyline R package](https://github.com/munterfinger/flexpolyline) -* [Simple Features for R](https://CRAN.R-project.org/package=sf) - -## License - -* The C++ implementation of the flexible polyline encoding by HERE Europe B.V. -is licensed under MIT. -* The **flexpolyline** R package is licensed under GNU GPL v3.0. diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt deleted file mode 100644 index 8db9579..0000000 --- a/cpp/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -cmake_minimum_required(VERSION 3.5) -project(flexpolyline) - -add_library(flexpolyline INTERFACE) -target_include_directories(flexpolyline INTERFACE include) - -add_executable(flexpolyline_cli src/cli.cpp) -set_property(TARGET flexpolyline_cli PROPERTY CXX_STANDARD 17) -target_link_libraries(flexpolyline_cli flexpolyline) - -if(MSVC) - target_compile_options(flexpolyline_cli PRIVATE /W4 /WX) -else() - target_compile_options(flexpolyline_cli PRIVATE -Wall -Wextra -pedantic -Werror) -endif() - -enable_testing() -add_executable(test_polyline test/test.cpp) -set_property(TARGET test_polyline PROPERTY CXX_STANDARD 17) -target_link_libraries(test_polyline flexpolyline) -add_test(NAME test_polyline COMMAND test_polyline) - -if(MSVC) - target_compile_options(test_polyline PRIVATE /W4 /WX) -else() - target_compile_options(test_polyline PRIVATE -Wall -Wextra -pedantic -Werror) -endif() \ No newline at end of file diff --git a/cpp/include/hf/flexpolyline.h b/cpp/include/hf/flexpolyline.h deleted file mode 100644 index 71cdc3d..0000000 --- a/cpp/include/hf/flexpolyline.h +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright (C) 2019 HERE Europe B.V. - * Licensed under MIT, see full license in LICENSE - * SPDX-License-Identifier: MIT - * License-Filename: LICENSE - */ -#include -#include -#include -#include -#include -#include -#include -#include - -/// # Flexible Polyline encoding -/// -/// The flexible polyline encoding is a lossy compressed representation of a list of coordinate -/// pairs or coordinate triples. It achieves that by: -/// -/// 1. Reducing the decimal digits of each value. -/// 2. Encoding only the offset from the previous point. -/// 3. Using variable length for each coordinate delta. -/// 4. Using 64 URL-safe characters to display the result. -/// -/// The encoding is a variant of [Encoded Polyline Algorithm Format]. The advantage of this encoding -/// over the original are the following: -/// -/// * Output string is composed by only URL-safe characters, i.e. may be used without URL encoding -/// as query parameters. -/// * Floating point precision is configurable: This allows to represent coordinates with precision -/// up to microns (5 decimal places allow meter precision only). -/// * It allows to encode a 3rd dimension with a given precision, which may be a level, altitude, -/// elevation or some other custom value. -/// -/// ## Specification -/// -/// See [Specification]. -/// -/// [Encoded Polyline Algorithm Format]: -/// https://developers.google.com/maps/documentation/utilities/polylinealgorithm -/// -/// [Specification]: https://github.com/heremaps/flexible-polyline#specifications -/// -/// ## Example -/// -/// ```cpp -/// #include< hf/flexpoly.h> -/// -/// // encode -/// std::vector> coordinates = {{50.1022829, 8.6982122}, -/// {50.1020076, 8.6956695}, -/// {50.1006313, 8.6914960}, -/// {50.0987800, 8.6875156}}; -/// hf::Polyline polyline = hf::Polyline2d{std::move(coordinates), *hf::Precision::from_u32(5)}; -/// -/// std::string encoded; -/// auto error = hf::polyline_encode(polyline, encoded)); -/// assert(!error); -/// assert(encoded == "BFoz5xJ67i1B1B7PzIhaxL7Y"); -/// -/// // decode -/// hf::Polyline result; -/// auto decode_error = hf::polyline_decode(encoded, result); -/// assert(!decode_error); -/// ``` -namespace hf::flexpolyline { - -/// Coordinate precision in the polyline -/// -/// Represents how many digits are to be encoded after the decimal point, e.g. -/// precision 3 would encode 4.456787 as 4.457. -/// -/// Supported values: `[0,16)` -class Precision { -public: - static std::optional from_u32(uint32_t value) { - if (value > 15) { - return {}; - } - return Precision(static_cast(value)); - } - uint32_t as_u32() const { return m_value; } - -private: - explicit Precision(uint8_t value) : m_value(value) {} - uint8_t m_value; -}; - -/// Informs about the type of the 3rd dimension of a 3D coordinate vector -enum class Type3d { - /// E.g. floor of a building - LEVEL = 1, - /// E.g. altitude (in the air) relative to ground level or mean sea level - ALTITUDE = 2, - /// E.g. elevation above mean-sea-level - ELEVATION = 3, - /// Reserved for future types - RESERVED1 = 4, - /// Reserved for future types - RESERVED2 = 5, - /// Reserved for custom types - CUSTOM1 = 6, - /// Reserved for custom types - CUSTOM2 = 7, -}; - -/// 2-dimensional polyline -struct Polyline2d { - /// List of 2D coordinates making up this polyline - std::vector> coordinates; - /// Precision of the coordinates (e.g. used for encoding, - /// or to report the precision supplied in encoded data) - Precision precision2d = *Precision::from_u32(7); - - Polyline2d() = default; - Polyline2d(std::vector> coordinates, Precision precision2d) - : coordinates(std::move(coordinates)), precision2d(precision2d) {} -}; - -/// 3-dimensional polyline -struct Polyline3d { - /// List of 3D coordinates making up this polyline - std::vector> coordinates; - /// Precision of the 2D part of the coordinates (e.g. used for encoding, - /// or to report the precision supplied in encoded data) - Precision precision2d = *Precision::from_u32(7); - /// Precision of the 3D part of the coordinates (e.g. used for encoding, - /// or to report the precision supplied in encoded data) - Precision precision3d = *Precision::from_u32(3); - /// Type of the 3D component - Type3d type3d = Type3d::ELEVATION; - - Polyline3d() = default; - Polyline3d(std::vector> coordinates, Precision precision2d, - Precision precision3d, Type3d type3d) - : coordinates(std::move(coordinates)), precision2d(precision2d), precision3d(precision3d), - type3d(type3d) {} -}; - -/// 2- or 3-dimensional polyline -using Polyline = std::variant; - -inline std::string to_string(const Polyline &polyline, std::optional precision = {}) { - std::ostringstream out; - out << std::fixed; - std::visit( - [&](auto &&arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - out << "{(" << arg.precision2d.as_u32() << "); ["; - out.precision(precision.value_or(arg.precision2d.as_u32())); - for (auto &coord : arg.coordinates) { - out << "(" << std::get<0>(coord) << ", " << std::get<1>(coord) << "), "; - } - out << "]}"; - } else if constexpr (std::is_same_v) { - out << "{(" << arg.precision2d.as_u32() << ", " << arg.precision3d.as_u32() << ", " - << static_cast(arg.type3d) << "); ["; - for (auto &coord : arg.coordinates) { - out.precision(precision.value_or(arg.precision2d.as_u32())); - out << "(" << std::get<0>(coord) << ", " << std::get<1>(coord); - out.precision(precision.value_or(arg.precision3d.as_u32())); - out << ", " << std::get<2>(coord) << "), "; - } - out << "]}"; - } else { - static_assert(sizeof(T) == 0, "non-exhaustive visitor!"); - } - }, - polyline); - return out.str(); -} - -enum class Error { - /// Data is encoded with unsupported version - UNSUPPORTED_VERSION, - /// Precision is not supported by encoding - INVALID_PRECISION, - /// Encoding is corrupt - INVALID_ENCODING, -}; - -/// Encodes a polyline into a string. -/// -/// The precision of the polyline is used to round coordinates, so the transformation is lossy -/// in nature. -inline std::optional polyline_encode(const Polyline &polyline, std::string &result) { - auto var_encode_u64 = [](uint64_t value, std::string &result) { - static const char *ENCODING_TABLE = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - - // var-length encode the number in chunks of 5 bits starting with the least significant - // to the most significant - while (value > 0x1F) { - uint32_t pos = (value & 0x1F) | 0x20; - char c = ENCODING_TABLE[pos]; - result.push_back(c); - value >>= 5; - } - char c = ENCODING_TABLE[value]; - result.push_back(c); - }; - - auto var_encode_i64 = [&](int64_t value, std::string &result) { - // make room on lowest bit - uint64_t encoded = static_cast(value) << 1; - - // invert bits if the value is negative - if (value < 0) { - encoded = ~encoded; - } - - var_encode_u64(encoded, result); - }; - - auto encode_header = [&](uint32_t precision2d, uint32_t precision3d, uint32_t type3d, - std::string &result) -> std::optional { - if (precision2d > 15 || precision3d > 15) { - return Error::INVALID_PRECISION; - } - var_encode_u64(1, result); // Version 1 - uint32_t header = (precision3d << 7) | (type3d) << 4 | precision2d; - var_encode_u64(header, result); - return {}; - }; - - auto precision_to_scale = [](Precision precision) { - double scale = std::pow(10, precision.as_u32()); - return [scale](double value) { return static_cast(std::round(value * scale)); }; - }; - - return std::visit( - [&](auto &&arg) -> std::optional { - using T = std::decay_t; - if constexpr (std::is_same_v) { - if (auto error = encode_header(arg.precision2d.as_u32(), 0, 0, result)) { - return *error; - } - auto scale2d = precision_to_scale(arg.precision2d); - - auto last_coord = std::make_tuple(0, 0); - for (auto coord : arg.coordinates) { - auto scaled_coord = - std::make_tuple(scale2d(std::get<0>(coord)), scale2d(std::get<1>(coord))); - var_encode_i64(std::get<0>(scaled_coord) - std::get<0>(last_coord), result); - var_encode_i64(std::get<1>(scaled_coord) - std::get<1>(last_coord), result); - last_coord = scaled_coord; - } - return {}; - } else if constexpr (std::is_same_v) { - if (auto error = encode_header(arg.precision2d.as_u32(), arg.precision3d.as_u32(), - static_cast(arg.type3d), result)) { - return *error; - } - auto scale2d = precision_to_scale(arg.precision2d); - auto scale3d = precision_to_scale(arg.precision3d); - - auto last_coord = std::make_tuple(0, 0, 0); - for (auto coord : arg.coordinates) { - auto scaled_coord = - std::make_tuple(scale2d(std::get<0>(coord)), scale2d(std::get<1>(coord)), - scale3d(std::get<2>(coord))); - var_encode_i64(std::get<0>(scaled_coord) - std::get<0>(last_coord), result); - var_encode_i64(std::get<1>(scaled_coord) - std::get<1>(last_coord), result); - var_encode_i64(std::get<2>(scaled_coord) - std::get<2>(last_coord), result); - last_coord = scaled_coord; - } - return {}; - } else { - static_assert(sizeof(T) == 0, "non-exhaustive visitor!"); - } - }, - polyline); -} - -/// Decodes an encoded polyline. -inline std::optional polyline_decode(std::string_view encoded, Polyline &result) { - - auto var_decode_u64 = [](std::string_view &bytes, uint64_t &result) -> std::optional { - static const int8_t DECODING_TABLE[] = { - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - }; - - result = 0; - uint8_t shift = 0; - - while (!bytes.empty()) { - uint8_t byte = bytes.front(); - bytes = std::string_view(bytes.data() + 1, bytes.size() - 1); - int8_t value = DECODING_TABLE[byte]; - if (value < 0) { - return Error::INVALID_ENCODING; - } - - result |= (static_cast(value) & 0x1F) << shift; - - if ((value & 0x20) == 0) { - return {}; - } - - shift += 5; - - if (shift >= 64) { - return Error::INVALID_ENCODING; - } - } - - return Error::INVALID_ENCODING; - }; - - auto var_decode_i64 = [&](std::string_view &bytes, int64_t &result) -> std::optional { - uint64_t value = 0; - if (auto error = var_decode_u64(bytes, value)) { - return *error; - } - bool negative = (value & 1) != 0; - value >>= 1; - if (negative) { - value = ~value; - } - result = static_cast(value); - return {}; - }; - - auto decode_header = [&](std::string_view &bytes, uint32_t &precision2d, uint32_t &precision3d, - uint32_t &type3d) -> std::optional { - uint64_t version = 0; - if (auto error = var_decode_u64(bytes, version)) { - return *error; - } - - if (version != 1) { - return Error::UNSUPPORTED_VERSION; - } - - uint64_t header = 0; - if (auto error = var_decode_u64(bytes, header)) { - return *error; - } - - if (header >= (static_cast(1) << 11)) { - return Error::INVALID_ENCODING; - } - precision2d = (header & 15); - type3d = ((header >> 4) & 7); - precision3d = ((header >> 7) & 15); - return {}; - }; - - uint32_t precision2d_encoded = 0; - uint32_t precision3d_encoded = 0; - uint32_t type3d_encoded = 0; - if (auto error = - decode_header(encoded, precision2d_encoded, precision3d_encoded, type3d_encoded)) { - return *error; - } - - std::optional type3d = [type3d_encoded] { - switch (type3d_encoded) { - case 1: - return std::optional(Type3d::LEVEL); - case 2: - return std::optional(Type3d::ALTITUDE); - case 3: - return std::optional(Type3d::ELEVATION); - case 4: - return std::optional(Type3d::RESERVED1); - case 5: - return std::optional(Type3d::RESERVED2); - case 6: - return std::optional(Type3d::CUSTOM1); - case 7: - return std::optional(Type3d::CUSTOM2); - default: - return std::optional(); - } - }(); - - auto precision2d = Precision::from_u32(precision2d_encoded); - if (!precision2d) { - return Error::INVALID_PRECISION; - } - - auto precision3d = Precision::from_u32(precision3d_encoded); - if (!precision3d) { - return Error::INVALID_PRECISION; - } - - auto precision_to_inverse_scale = [](uint32_t precision) { - double scale = std::pow(10, precision); - return [scale](int64_t value) { return static_cast(value) / scale; }; - }; - - auto decode3d = - [&](std::string_view &bytes, uint32_t precision2d, uint32_t precision3d, - std::vector> &result) -> std::optional { - result.reserve(bytes.size() / 2); - auto scale2d = precision_to_inverse_scale(precision2d); - auto scale3d = precision_to_inverse_scale(precision3d); - auto last_coord = std::make_tuple(0, 0, 0); - while (!bytes.empty()) { - auto delta = std::make_tuple(0, 0, 0); - if (auto error = var_decode_i64(bytes, std::get<0>(delta))) { - return *error; - } - if (auto error = var_decode_i64(bytes, std::get<1>(delta))) { - return *error; - } - if (auto error = var_decode_i64(bytes, std::get<2>(delta))) { - return *error; - } - std::get<0>(last_coord) += std::get<0>(delta); - std::get<1>(last_coord) += std::get<1>(delta); - std::get<2>(last_coord) += std::get<2>(delta); - result.emplace_back(scale2d(std::get<0>(last_coord)), scale2d(std::get<1>(last_coord)), - scale3d(std::get<2>(last_coord))); - }; - return {}; - }; - - auto decode2d = [&](std::string_view &bytes, uint32_t precision2d, - std::vector> &result) -> std::optional { - result.reserve(bytes.size() / 2); - auto scale2d = precision_to_inverse_scale(precision2d); - auto last_coord = std::make_tuple(0, 0); - while (!bytes.empty()) { - auto delta = std::make_tuple(0, 0); - if (auto error = var_decode_i64(bytes, std::get<0>(delta))) { - return *error; - } - if (auto error = var_decode_i64(bytes, std::get<1>(delta))) { - return *error; - } - std::get<0>(last_coord) += std::get<0>(delta); - std::get<1>(last_coord) += std::get<1>(delta); - result.emplace_back(scale2d(std::get<0>(last_coord)), scale2d(std::get<1>(last_coord))); - } - return {}; - }; - - if (type3d) { - std::vector> coordinates; - if (auto error = decode3d(encoded, precision2d_encoded, precision3d_encoded, coordinates)) { - return *error; - } - result = Polyline3d{std::move(coordinates), *precision2d, *precision3d, *type3d}; - } else { - std::vector> coordinates; - if (auto error = decode2d(encoded, precision2d_encoded, coordinates)) { - return *error; - } - result = Polyline2d{std::move(coordinates), *precision2d}; - } - return {}; -} -} // namespace hf::flexpolyline diff --git a/cpp/src/cli.cpp b/cpp/src/cli.cpp deleted file mode 100644 index 3d07853..0000000 --- a/cpp/src/cli.cpp +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2021 HERE Europe B.V. - * Licensed under MIT, see full license in LICENSE - * SPDX-License-Identifier: MIT - * License-Filename: LICENSE - */ - -#include -#include - -#include - -std::string_view remove_decoration(std::string_view x, std::string_view prefix, - std::string_view suffix) { - if (x.size() < suffix.size() + suffix.size() || x.substr(0, prefix.size()) != prefix || - x.substr(x.size() - suffix.length(), suffix.length()) != suffix) { - throw std::runtime_error(std::string(prefix) + suffix.data() + " missing"); - } - x.remove_prefix(prefix.size()); - x.remove_suffix(suffix.size()); - return x; -} - -std::string_view split_next(std::string_view &x, std::string_view sep, bool required) { - auto pos = x.find(sep); - auto result = x.substr(0, pos); - if (pos == std::string::npos) { - if (required) { - throw std::runtime_error(std::string("Missing seperator: ") + sep.data()); - } - x = std::string_view(); - } else { - x = x.substr(pos + sep.size()); - } - return result; -} - -hf::flexpolyline::Precision parse_precision(std::string_view x) { - uint32_t prec_u32 = std::atoi(x.data()); - if (auto prec = hf::flexpolyline::Precision::from_u32(prec_u32)) { - return *prec; - } - throw std::runtime_error("Precision outside of supported range: " + std::to_string(prec_u32)); -} - -hf::flexpolyline::Type3d parse_3d_type(std::string_view x) { - uint32_t value_u32 = std::atoi(x.data()); - switch (value_u32) { - case 1: - return hf::flexpolyline::Type3d::LEVEL; - case 2: - return hf::flexpolyline::Type3d::ALTITUDE; - case 3: - return hf::flexpolyline::Type3d::ELEVATION; - case 4: - return hf::flexpolyline::Type3d::RESERVED1; - case 5: - return hf::flexpolyline::Type3d::RESERVED2; - case 6: - return hf::flexpolyline::Type3d::CUSTOM1; - case 7: - return hf::flexpolyline::Type3d::CUSTOM2; - default: - throw std::runtime_error("Unexpected 3d type: " + std::to_string(value_u32)); - } -} - -hf::flexpolyline::Polyline from_str(std::string_view input) { - - auto data = remove_decoration(input, "{", "}"); - auto header = remove_decoration(split_next(data, "; ", true), "(", ")"); - auto coords_data = remove_decoration(data, "[(", "), ]"); - auto precision2d = parse_precision(split_next(header, ", ", false)); - if (header.empty()) { - std::vector> coordinates; - while (!coords_data.empty()) { - auto lat_str = split_next(coords_data, ", ", true); - double lat = std::atof(lat_str.data()); - auto lng_str = split_next(coords_data, "), (", false); - double lng = std::atof(lng_str.data()); - - coordinates.emplace_back(lat, lng); - } - return hf::flexpolyline::Polyline2d{std::move(coordinates), precision2d}; - } else { - auto precision3d = parse_precision(split_next(header, ", ", true)); - auto type3d = parse_3d_type(header); - std::vector> coordinates; - while (!coords_data.empty()) { - auto lat_str = split_next(coords_data, ", ", true); - double lat = std::atof(lat_str.data()); - auto lng_str = split_next(coords_data, ", ", true); - double lng = std::atof(lng_str.data()); - auto third_str = split_next(coords_data, "), (", false); - double third = std::atof(third_str.data()); - - coordinates.emplace_back(lat, lng, third); - } - return hf::flexpolyline::Polyline3d{std::move(coordinates), precision2d, precision3d, type3d}; - } -} - -int main(int argc, const char *argv[]) { - if (argc != 2 || - (std::string_view(argv[1]) != "encode" && std::string_view(argv[1]) != "decode")) { - std::cerr << "Usage: flexpolyline encode|decode" << std::endl; - std::cerr << " input: stdin" << std::endl; - std::cerr << " output: stdout" << std::endl; - return 1; - } - - try { - std::string input_line; - while (std::cin) { - std::getline(std::cin, input_line); - if (input_line.empty()) { - continue; - } - if (std::string_view(argv[1]) == "encode") { - auto polyline = from_str(input_line); - std::string result; - if (auto error = hf::flexpolyline::polyline_encode(polyline, result)) { - std::cerr << "Failed to encode: " << static_cast(*error) << std::endl; - return 1; - } - std::cout << result << std::endl; - } else if (std::string_view(argv[1]) == "decode") { - hf::flexpolyline::Polyline result; - if (auto error = hf::flexpolyline::polyline_decode(input_line, result)) { - std::cerr << "Failed to decode: " << static_cast(*error) << std::endl; - return 1; - } - std::cout << hf::flexpolyline::to_string(result, 15) << std::endl; - } else { - std::cerr << "Command not recognized: " << argv[1] << std::endl; - return 1; - } - } - } catch (std::exception &e) { - std::cerr << "[ERROR] " << e.what() << std::endl; - return -1; - } - - return 0; -} diff --git a/cpp/test/test.cpp b/cpp/test/test.cpp deleted file mode 100644 index fb0729c..0000000 --- a/cpp/test/test.cpp +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (C) 2019 HERE Europe B.V. - * Licensed under MIT, see full license in LICENSE - * SPDX-License-Identifier: MIT - * License-Filename: LICENSE - */ -#include -#include -#include -#include -#include - -using namespace hf::flexpolyline; - -void check_encode_decode(const Polyline &poly, const std::string &reference_encoded, - const Polyline &reference_decoded) { - std::string result; - if (auto error = polyline_encode(poly, result)) { - std::cerr << "Failed to encode " << to_string(poly) << std::endl; - std::exit(1); - } - if (result != reference_encoded) { - std::cerr << "Encoded " << to_string(poly) << std::endl - << "Got " << result << std::endl - << "Expected " << reference_encoded << std::endl; - std::exit(1); - } - - Polyline decoded; - if (auto error = polyline_decode(reference_encoded, decoded)) { - std::cerr << "Failed to decode " << reference_encoded << std::endl; - std::exit(1); - } - - if (to_string(reference_decoded) != to_string(decoded)) { - std::cerr << "Decoded " << reference_encoded << std::endl - << "Got " << to_string(decoded) << std::endl - << "Expected " << to_string(poly) << std::endl; - std::exit(1); - } -} - -void test_2d_example_1() { - std::vector> coordinates = {{50.1022829, 8.6982122}, - {50.1020076, 8.6956695}, - {50.1006313, 8.6914960}, - {50.0987800, 8.6875156}}; - - std::vector> coordinates_result = { - {50.102280, 8.698210}, {50.102010, 8.695670}, {50.100630, 8.691500}, {50.098780, 8.687520}}; - - check_encode_decode(Polyline2d{coordinates, *Precision::from_u32(5)}, "BFoz5xJ67i1B1B7PzIhaxL7Y", - Polyline2d{coordinates_result, *Precision::from_u32(5)}); -} - -void test_2d_example_2() { - std::vector> coordinates = { - {52.5199356, 13.3866272}, {52.5100899, 13.2816896}, {52.4351807, 13.1935196}, - {52.4107285, 13.1964502}, {52.3887100, 13.1557798}, {52.3727798, 13.1491003}, - {52.3737488, 13.1154604}, {52.3875198, 13.0872202}, {52.4029388, 13.0706196}, - {52.4105797, 13.0755529}}; - - std::vector> coordinates_result = { - {52.519940, 13.386630}, {52.510090, 13.281690}, {52.435180, 13.193520}, - {52.410730, 13.196450}, {52.388710, 13.155780}, {52.372780, 13.149100}, - {52.373750, 13.115460}, {52.387520, 13.087220}, {52.402940, 13.070620}, - {52.410580, 13.075550}}; - - check_encode_decode(Polyline2d{coordinates, *Precision::from_u32(5)}, - "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e", - Polyline2d{coordinates_result, *Precision::from_u32(5)}); -} - -void test_3d_example_1() { - std::vector> coordinates = {{50.1022829, 8.6982122, 10.0}, - {50.1020076, 8.6956695, 20.0}, - {50.1006313, 8.6914960, 30.0}, - {50.0987800, 8.6875156, 40.0}}; - - std::vector> coordinates_result = { - {50.102280, 8.698210, 10.0}, - {50.102010, 8.695670, 20.0}, - {50.100630, 8.691500, 30.0}, - {50.098780, 8.687520, 40.0}}; - - check_encode_decode( - Polyline3d{coordinates, *Precision::from_u32(5), *Precision::from_u32(0), Type3d::LEVEL}, - "BVoz5xJ67i1BU1B7PUzIhaUxL7YU", - Polyline3d{coordinates_result, *Precision::from_u32(5), *Precision::from_u32(0), - Type3d::LEVEL}); -} - -void test_rounding_2d() { - std::vector> coordinate_values = { - {96821474666297905, 78334196549606266}, {29405294060895017, 70361389340728572}, - {16173544634348013, 17673855782924183}, {22448654820449524, 13005139703027850}, - {73351231936757857, 78298027377720633}, {78008331957098324, 4847613123220218}, - {62755680515396509, 49165433608990700}, {93297154866561429, 52373802822465027}, - {89973844644540399, 75975762025877533}, {48555821719956867, 31591090068957813}}; - - for (uint32_t precision2d = 0; precision2d < 16; precision2d++) { - auto to_f64 = [](const std::tuple &value) { - return std::make_tuple(static_cast(std::get<0>(value)) / std::pow(10, 15), - static_cast(std::get<1>(value)) / std::pow(10, 15)); - }; - - auto to_rounded_f64 = [&](const std::tuple &input) { - auto value = to_f64(input); - auto scale = std::pow(10, precision2d); - return std::make_tuple(std::round(std::get<0>(value) * scale) / scale, - std::round(std::get<1>(value) * scale) / scale); - }; - - Polyline2d expected; - expected.precision2d = *Precision::from_u32(precision2d); - for (auto coord : coordinate_values) { - expected.coordinates.emplace_back(to_rounded_f64(coord)); - } - - Polyline2d actual; - actual.precision2d = *Precision::from_u32(precision2d); - for (auto coord : coordinate_values) { - actual.coordinates.emplace_back(to_f64(coord)); - } - - std::string expected_encoded; - if (auto error = polyline_encode(expected, expected_encoded)) { - std::cerr << "Failed to encode " << to_string(expected) << std::endl; - std::exit(1); - } - - std::string actual_encoded; - if (auto error = polyline_encode(actual, actual_encoded)) { - std::cerr << "Failed to encode " << to_string(actual) << std::endl; - std::exit(1); - } - - if (expected_encoded != actual_encoded) { - std::cerr << "Precision " << precision2d << std::endl - << "Expected " << expected_encoded << std::endl - << "Got " << actual_encoded << std::endl; - exit(1); - } - } -} - -void test_rounding_3d() { - std::vector> coordinate_values = { - {96821474666297905, 78334196549606266, 23131023979661380}, - {29405294060895017, 70361389340728572, 81917934930416924}, - {16173544634348013, 17673855782924183, 86188502094968953}, - {22448654820449524, 13005139703027850, 68774670569614983}, - {73351231936757857, 78298027377720633, 52078352171243855}, - {78008331957098324, 4847613123220218, 6550838806837986}, - {62755680515396509, 49165433608990700, 39041897671300539}, - {93297154866561429, 52373802822465027, 67310807938230681}, - {89973844644540399, 75975762025877533, 66789448009436096}, - {48555821719956867, 31591090068957813, 49203621966471323}}; - - uint32_t precision2d = 5; - for (uint32_t precision3d = 0; precision3d < 16; precision3d++) { - for (auto type3d : { - Type3d::LEVEL, - Type3d::ALTITUDE, - Type3d::ELEVATION, - Type3d::RESERVED1, - Type3d::RESERVED2, - Type3d::CUSTOM1, - Type3d::CUSTOM2, - }) { - auto to_f64 = [](const std::tuple &value) { - return std::make_tuple(static_cast(std::get<0>(value)) / std::pow(10, 15), - static_cast(std::get<1>(value)) / std::pow(10, 15), - static_cast(std::get<2>(value)) / std::pow(10, 15)); - }; - - auto to_rounded_f64 = [&](const std::tuple &input) { - auto value = to_f64(input); - auto scale2d = std::pow(10, precision2d); - auto scale3d = std::pow(10, precision3d); - return std::make_tuple(std::round(std::get<0>(value) * scale2d) / scale2d, - std::round(std::get<1>(value) * scale2d) / scale2d, - std::round(std::get<2>(value) * scale3d) / scale3d); - }; - - Polyline3d expected; - expected.precision2d = *Precision::from_u32(precision2d); - expected.precision3d = *Precision::from_u32(precision3d); - expected.type3d = type3d; - for (auto coord : coordinate_values) { - expected.coordinates.emplace_back(to_rounded_f64(coord)); - } - - Polyline3d actual; - actual.precision2d = *Precision::from_u32(precision2d); - actual.precision3d = *Precision::from_u32(precision3d); - actual.type3d = type3d; - for (auto coord : coordinate_values) { - actual.coordinates.emplace_back(to_f64(coord)); - } - - std::string expected_encoded; - if (auto error = polyline_encode(expected, expected_encoded)) { - std::cerr << "Failed to encode " << to_string(expected) << std::endl; - std::exit(1); - } - - std::string actual_encoded; - if (auto error = polyline_encode(actual, actual_encoded)) { - std::cerr << "Failed to encode " << to_string(actual) << std::endl; - std::exit(1); - } - - if (expected_encoded != actual_encoded) { - std::cerr << "Precision " << precision2d << std::endl - << "Expected " << expected_encoded << std::endl - << "Got " << actual_encoded << std::endl; - exit(1); - } - } - } -} - -int main(int, char const *[]) { - std::cout << "Running tests" << std::endl; - test_2d_example_1(); - test_2d_example_2(); - test_3d_example_1(); - test_rounding_2d(); - test_rounding_3d(); - std::cout << "Done" << std::endl; - return 0; -} diff --git a/dotnet/HERE.FlexiblePolyline.sln b/dotnet/HERE.FlexiblePolyline.sln deleted file mode 100644 index 072ad40..0000000 --- a/dotnet/HERE.FlexiblePolyline.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29911.84 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HERE.FlexiblePolyline", "src\HERE.FlexiblePolyline.csproj", "{43B7F4E7-7D7D-4FE9-A6AB-ACF6F4760975}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexiblePolylineEncoder.Tests", "test\FlexiblePolylineEncoder.Tests\FlexiblePolylineEncoder.Tests.csproj", "{58A3B4E0-A356-493F-AAD1-0BB7C8FE4D5C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {43B7F4E7-7D7D-4FE9-A6AB-ACF6F4760975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {43B7F4E7-7D7D-4FE9-A6AB-ACF6F4760975}.Debug|Any CPU.Build.0 = Debug|Any CPU - {43B7F4E7-7D7D-4FE9-A6AB-ACF6F4760975}.Release|Any CPU.ActiveCfg = Release|Any CPU - {43B7F4E7-7D7D-4FE9-A6AB-ACF6F4760975}.Release|Any CPU.Build.0 = Release|Any CPU - {58A3B4E0-A356-493F-AAD1-0BB7C8FE4D5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {58A3B4E0-A356-493F-AAD1-0BB7C8FE4D5C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {58A3B4E0-A356-493F-AAD1-0BB7C8FE4D5C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {58A3B4E0-A356-493F-AAD1-0BB7C8FE4D5C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {CBF7FB00-A4E8-4249-A71C-41043293767D} - EndGlobalSection -EndGlobal diff --git a/dotnet/src/HERE.FlexiblePolyline.csproj b/dotnet/src/HERE.FlexiblePolyline.csproj deleted file mode 100644 index 1a37104..0000000 --- a/dotnet/src/HERE.FlexiblePolyline.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - netstandard2.0 - - - - - - - diff --git a/dotnet/src/LatLngZ.cs b/dotnet/src/LatLngZ.cs deleted file mode 100644 index 446d650..0000000 --- a/dotnet/src/LatLngZ.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; - -namespace HERE.FlexiblePolyline -{ - /// - /// Coordinate triple - /// - public class LatLngZ - { - public double Lat { get; } - public double Lng { get; } - public double Z { get; } - - public LatLngZ(double latitude, double longitude, double thirdDimension = 0) - { - Lat = latitude; - Lng = longitude; - Z = thirdDimension; - } - - public override string ToString() - { - return "LatLngZ [lat=" + Lat + ", lng=" + Lng + ", z=" + Z + "]"; - } - - public override bool Equals(object obj) - { - if (this == obj) - { - return true; - } - - if (obj is LatLngZ latLngZ) - { - if (latLngZ.Lat == Lat && latLngZ.Lng == Lng && latLngZ.Z == Z) - { - return true; - } - } - - return false; - } - - public override int GetHashCode() - { - return HashCode.Combine(Lat, Lng, Z); - } - } -} diff --git a/dotnet/src/PolylineEncoderDecoder.cs b/dotnet/src/PolylineEncoderDecoder.cs deleted file mode 100644 index a05998a..0000000 --- a/dotnet/src/PolylineEncoderDecoder.cs +++ /dev/null @@ -1,410 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace HERE.FlexiblePolyline -{ - public class PolylineEncoderDecoder - { - /// Header version - /// A change in the version may affect the logic to encode and decode the rest of the header and data - /// - private static readonly byte FORMAT_VERSION = 1; - - // Base64 URL-safe characters - private static readonly char[] ENCODING_TABLE = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".ToCharArray(); - - private static readonly int[] DECODING_TABLE = - { - 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 - }; - - /// - /// Encode the list of coordinate triples. - /// The third dimension value will be eligible for encoding only when ThirdDimension is other than ABSENT. - /// This is lossy compression based on precision accuracy. - /// - /// coordinates {@link List} of coordinate triples that to be encoded. - /// Floating point precision of the coordinate to be encoded. - /// {@link ThirdDimension} which may be a level, altitude, elevation or some other custom value - /// Floating point precision for thirdDimension value - /// URL-safe encoded {@link String} for the given coordinates. - public static string Encode(List coordinates, int precision, ThirdDimension thirdDimension, int thirdDimPrecision) - { - if (coordinates == null || coordinates.Count == 0) - { - throw new ArgumentException("Invalid coordinates!"); - } - - if (!Enum.IsDefined(typeof(ThirdDimension), thirdDimension)) - { - throw new ArgumentException("Invalid thirdDimension"); - //thirdDimension = ThirdDimension.Absent; - } - - Encoder enc = new Encoder(precision, thirdDimension, thirdDimPrecision); - foreach (var coordinate in coordinates) - { - enc.Add(coordinate); - } - - return enc.GetEncoded(); - } - - /// - /// Decode the encoded input {@link String} to {@link List} of coordinate triples. - /// @see PolylineDecoder#getThirdDimension(String) getThirdDimension - /// @see LatLngZ - /// - /// encoded URL-safe encoded {@link String} - /// {@link List} of coordinate triples that are decoded from input - public static List Decode(string encoded) - { - if (string.IsNullOrEmpty(encoded?.Trim())) - { - throw new ArgumentException("Invalid argument!", nameof(encoded)); - } - - List result = new List(); - Decoder dec = new Decoder(encoded); - - double lat = 0; - double lng = 0; - double z = 0; - - while (dec.DecodeOne(ref lat, ref lng, ref z)) - { - result.Add(new LatLngZ(lat, lng, z)); - lat = 0; - lng = 0; - z = 0; - } - - return result; - } - - /// - /// ThirdDimension type from the encoded input {@link String} - /// - /// URL-safe encoded coordinate triples {@link String} - public static ThirdDimension GetThirdDimension(string encoded) - { - int index = 0; - long header = 0; - Decoder.DecodeHeaderFromString(encoded.ToCharArray(), ref index, ref header); - return (ThirdDimension)((header >> 4) & 0b_111); - } - - public byte GetVersion() - { - return FORMAT_VERSION; - } - - /// - /// Internal class for configuration, validation and encoding for an input request. - /// - private class Encoder - { - private readonly StringBuilder _result; - private readonly Converter _latConverter; - private readonly Converter _lngConverter; - private readonly Converter _zConverter; - private readonly ThirdDimension _thirdDimension; - - public Encoder(int precision, ThirdDimension thirdDimension, int thirdDimPrecision) - { - _latConverter = new Converter(precision); - _lngConverter = new Converter(precision); - _zConverter = new Converter(thirdDimPrecision); - _thirdDimension = thirdDimension; - _result = new StringBuilder(); - EncodeHeader(precision, (int)_thirdDimension, thirdDimPrecision); - } - - private void EncodeHeader(int precision, int thirdDimensionValue, int thirdDimPrecision) - { - // Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char - if (precision < 0 || precision > 15) - { - throw new ArgumentException("precision out of range"); - } - - if (thirdDimPrecision < 0 || thirdDimPrecision > 15) - { - throw new ArgumentException("thirdDimPrecision out of range"); - } - - if (thirdDimensionValue < 0 || thirdDimensionValue > 7) - { - throw new ArgumentException("thirdDimensionValue out of range"); - } - - long res = (thirdDimPrecision << 7) | (thirdDimensionValue << 4) | precision; - Converter.EncodeUnsignedVarint(PolylineEncoderDecoder.FORMAT_VERSION, _result); - Converter.EncodeUnsignedVarint(res, _result); - } - - private void Add(double lat, double lng) - { - _latConverter.EncodeValue(lat, _result); - _lngConverter.EncodeValue(lng, _result); - } - - private void Add(double lat, double lng, double z) - { - Add(lat, lng); - if (_thirdDimension != ThirdDimension.Absent) - { - _zConverter.EncodeValue(z, _result); - } - } - - public void Add(LatLngZ tuple) - { - if (tuple == null) - { - throw new ArgumentNullException(nameof(tuple), "Invalid LatLngZ tuple"); - } - - Add(tuple.Lat, tuple.Lng, tuple.Z); - } - - public string GetEncoded() - { - return _result.ToString(); - } - } - - /// - /// Single instance for decoding an input request. - /// - private class Decoder - { - private readonly char[] _encoded; - private int _index; - private readonly Converter _latConverter; - private readonly Converter _lngConverter; - private readonly Converter _zConverter; - - private int _precision; - private int _thirdDimPrecision; - private ThirdDimension _thirdDimension; - - - public Decoder(string encoded) - { - _encoded = encoded.ToCharArray(); - _index = 0; - DecodeHeader(); - _latConverter = new Converter(_precision); - _lngConverter = new Converter(_precision); - _zConverter = new Converter(_thirdDimPrecision); - } - - private bool HasThirdDimension() - { - return _thirdDimension != ThirdDimension.Absent; - } - - private void DecodeHeader() - { - long header = 0; - DecodeHeaderFromString(_encoded, ref _index, ref header); - _precision = (int)(header & 0b_1111); // we pick the first 4 bits only - header >>= 4; - _thirdDimension = (ThirdDimension)(header & 0b_111); // we pick the first 3 bits only - _thirdDimPrecision = (int)((header >> 3) & 0b_1111); - } - - public static void DecodeHeaderFromString(char[] encoded, ref int index, ref long header) - { - long value = 0; - - // Decode the header version - if (!Converter.DecodeUnsignedVarint(encoded, ref index, ref value)) - { - throw new ArgumentException("Invalid encoding"); - } - - if (value != FORMAT_VERSION) - { - throw new ArgumentException("Invalid format version"); - } - - // Decode the polyline header - if (!Converter.DecodeUnsignedVarint(encoded, ref index, ref value)) - { - throw new ArgumentException("Invalid encoding"); - } - - header = value; - } - - - public bool DecodeOne( - ref double lat, - ref double lng, - ref double z) - { - if (_index == _encoded.Length) - { - return false; - } - - if (!_latConverter.DecodeValue(_encoded, ref _index, ref lat)) - { - throw new ArgumentException("Invalid encoding"); - } - - if (!_lngConverter.DecodeValue(_encoded, ref _index, ref lng)) - { - throw new ArgumentException("Invalid encoding"); - } - - if (HasThirdDimension()) - { - if (!_zConverter.DecodeValue(_encoded, ref _index, ref z)) - { - throw new ArgumentException("Invalid encoding"); - } - } - - return true; - } - } - - //Decode a single char to the corresponding value - private static int DecodeChar(char charValue) - { - int pos = charValue - 45; - if (pos < 0 || pos > 77) - { - return -1; - } - - return DECODING_TABLE[pos]; - } - - /// - /// Stateful instance for encoding and decoding on a sequence of Coordinates part of an request. - /// Instance should be specific to type of coordinates (e.g. Lat, Lng) - /// so that specific type delta is computed for encoding. - /// Lat0 Lng0 3rd0 (Lat1-Lat0) (Lng1-Lng0) (3rdDim1-3rdDim0) - /// - public class Converter - { - private long _multiplier = 0; - private long _lastValue = 0; - - public Converter(int precision) - { - SetPrecision(precision); - } - - private void SetPrecision(int precision) - { - _multiplier = (long)Math.Pow(10, precision); - } - - public static void EncodeUnsignedVarint(long value, StringBuilder result) - { - while (value > 0x1F) - { - byte pos = (byte)((value & 0x1F) | 0x20); - result.Append(ENCODING_TABLE[pos]); - value >>= 5; - } - - result.Append(ENCODING_TABLE[(byte)value]); - } - - public void EncodeValue(double value, StringBuilder result) - { - /* - * Round-half-up - * round(-1.4) --> -1 - * round(-1.5) --> -2 - * round(-2.5) --> -3 - */ - long scaledValue = (long)Math.Round(Math.Abs(value * _multiplier), MidpointRounding.AwayFromZero) * Math.Sign(value); - long delta = scaledValue - _lastValue; - bool negative = delta < 0; - - _lastValue = scaledValue; - - // make room on lowest bit - delta <<= 1; - - // invert bits if the value is negative - if (negative) - { - delta = ~delta; - } - - EncodeUnsignedVarint(delta, result); - } - - public static bool DecodeUnsignedVarint(char[] encoded, - ref int index, - ref long result) - { - short shift = 0; - long delta = 0; - - while (index < encoded.Length) - { - long value = DecodeChar(encoded[index]); - if (value < 0) - { - return false; - } - - index++; - delta |= (value & 0x1F) << shift; - if ((value & 0x20) == 0) - { - result = delta; - return true; - } - else - { - shift += 5; - } - } - - if (shift > 0) - { - return false; - } - - return true; - } - - //Decode single coordinate (say lat|lng|z) starting at index - public bool DecodeValue(char[] encoded, - ref int index, - ref double coordinate) - { - long delta = 0; - if (!DecodeUnsignedVarint(encoded, ref index, ref delta)) - { - return false; - } - - if ((delta & 1) != 0) - { - delta = ~delta; - } - - delta >>= 1; - _lastValue += delta; - coordinate = (double)_lastValue / _multiplier; - return true; - } - } - } -} diff --git a/dotnet/src/ThirdDimension.cs b/dotnet/src/ThirdDimension.cs deleted file mode 100644 index 9e32150..0000000 --- a/dotnet/src/ThirdDimension.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace HERE.FlexiblePolyline -{ - /// - /// 3rd dimension specification. - /// Example a level, altitude, elevation or some other custom value. - /// ABSENT is default when there is no third dimension en/decoding required. - /// - public enum ThirdDimension - { - Absent = 0, - Level = 1, - Altitude = 2, - Elevation = 3, - Reserved1 = 4, - Reserved2 = 5, - Custom1 = 6, - Custom2 = 7 - } -} diff --git a/dotnet/test/FlexiblePolylineEncoder.Tests/FlexiblePolylineEncoder.Tests.csproj b/dotnet/test/FlexiblePolylineEncoder.Tests/FlexiblePolylineEncoder.Tests.csproj deleted file mode 100644 index 7fdb68a..0000000 --- a/dotnet/test/FlexiblePolylineEncoder.Tests/FlexiblePolylineEncoder.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - netcoreapp3.1 - - false - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - - diff --git a/dotnet/test/FlexiblePolylineEncoder.Tests/FlexiblePolylineEncoderTests.cs b/dotnet/test/FlexiblePolylineEncoder.Tests/FlexiblePolylineEncoderTests.cs deleted file mode 100644 index 4a4cbe4..0000000 --- a/dotnet/test/FlexiblePolylineEncoder.Tests/FlexiblePolylineEncoderTests.cs +++ /dev/null @@ -1,339 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; -using HERE.FlexiblePolyline; -using NUnit.Framework; - -namespace FlexiblePolylineEncoder.Tests -{ - public class FlexiblePolylineEncoderTests - { - [Test] - public void EncodedFlexilineMatchesDecodedResultTest() - { - using (var decodedEnumerator = File.ReadLines("../../../../../../test/round_half_up/decoded.txt").GetEnumerator()) - { - foreach (var flexiline in File.ReadLines("../../../../../../test/round_half_up/encoded.txt")) - { - decodedEnumerator.MoveNext(); - var testResultStr = decodedEnumerator.Current; - - var expectedResult = ParseExpectedTestResult(testResultStr); - - var decoded = PolylineEncoderDecoder.Decode(flexiline); - var encoded = PolylineEncoderDecoder.Encode( - decoded, - expectedResult.Precision.Precision2d, - expectedResult.Precision.Type3d, - expectedResult.Precision.Precision3d); - - ThirdDimension thirdDimension = PolylineEncoderDecoder.GetThirdDimension(encoded); - Assert.AreEqual(expectedResult.Precision.Type3d, thirdDimension); - - for (int i = 0; i < decoded.Count; i++) - { - AssertEqualWithPrecision( - expectedResult.Coordinates[i].Lat, - decoded[i].Lat, - expectedResult.Precision.Precision2d); - - AssertEqualWithPrecision( - expectedResult.Coordinates[i].Lng, - decoded[i].Lng, - expectedResult.Precision.Precision2d); - - AssertEqualWithPrecision( - expectedResult.Coordinates[i].Z, - decoded[i].Z, - expectedResult.Precision.Precision3d); - } - - if (flexiline != encoded) - { - Console.WriteLine($@"WARNING expected {flexiline} but got {encoded}"); - } - } - } - } - - private void AssertEqualWithPrecision(double expected, double actual, int precision) - { - long expectedLong = (long)(Math.Round(expected * (int)Math.Pow(10, precision), MidpointRounding.AwayFromZero)); - long actualLong = (long)(Math.Round(actual * (int)Math.Pow(10, precision), MidpointRounding.AwayFromZero)); - - Assert.AreEqual(expectedLong, actualLong); - } - - [Test] - public void TestInvalidCoordinates() - { - - //Null coordinates - Assert.Throws(() => - { - PolylineEncoderDecoder.Encode(null, 5, ThirdDimension.Absent, 0); - }); - - - //Empty coordinates list test - Assert.Throws(() => - { - PolylineEncoderDecoder.Encode(new List(), 5, ThirdDimension.Absent, 0); - }); - } - - [Test] - public void TestInvalidThirdDimension() - { - - var pairs = new List(); - pairs.Add(new LatLngZ(50.1022829, 8.6982122)); - ThirdDimension invalid = (ThirdDimension)999; - Assert.Throws(() => - { - PolylineEncoderDecoder.Encode(pairs, 5, invalid, 0); - }); - } - - [Test] - public void TestConvertValue() - { - PolylineEncoderDecoder.Converter conv = new PolylineEncoderDecoder.Converter(5); - StringBuilder result = new StringBuilder(); - conv.EncodeValue(-179.98321, result); - Assert.AreEqual(result.ToString(), "h_wqiB"); - } - - [Test] - public void TestSimpleLatLngEncoding() - { - var pairs = new List(); - pairs.Add(new LatLngZ(50.1022829, 8.6982122)); - pairs.Add(new LatLngZ(50.1020076, 8.6956695)); - pairs.Add(new LatLngZ(50.1006313, 8.6914960)); - pairs.Add(new LatLngZ(50.0987800, 8.6875156)); - - var expected = "BFoz5xJ67i1B1B7PzIhaxL7Y"; - var computed = PolylineEncoderDecoder.Encode(pairs, 5, ThirdDimension.Absent, 0); - Assert.AreEqual(computed, expected); - } - - [Test] - public void TestComplexLatLngEncoding() - { - - var pairs = new List(); - pairs.Add(new LatLngZ(52.5199356, 13.3866272)); - pairs.Add(new LatLngZ(52.5100899, 13.2816896)); - pairs.Add(new LatLngZ(52.4351807, 13.1935196)); - pairs.Add(new LatLngZ(52.4107285, 13.1964502)); - pairs.Add(new LatLngZ(52.38871, 13.1557798)); - pairs.Add(new LatLngZ(52.3727798, 13.1491003)); - pairs.Add(new LatLngZ(52.3737488, 13.1154604)); - pairs.Add(new LatLngZ(52.3875198, 13.0872202)); - pairs.Add(new LatLngZ(52.4029388, 13.0706196)); - pairs.Add(new LatLngZ(52.4105797, 13.0755529)); - - var expected = "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e"; - var computed = PolylineEncoderDecoder.Encode(pairs, 5, ThirdDimension.Absent, 0); - Assert.AreEqual(computed, expected); - } - - [Test] - public void TestLatLngZEncode() - { - - var tuples = new List(); - tuples.Add(new LatLngZ(50.1022829, 8.6982122, 10)); - tuples.Add(new LatLngZ(50.1020076, 8.6956695, 20)); - tuples.Add(new LatLngZ(50.1006313, 8.6914960, 30)); - tuples.Add(new LatLngZ(50.0987800, 8.6875156, 40)); - - var expected = "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU"; - var computed = PolylineEncoderDecoder.Encode(tuples, 5, ThirdDimension.Altitude, 0); - Assert.AreEqual(computed, expected); - } - /**********************************************/ - /********** Decoder test starts ***************/ - /**********************************************/ - [Test] - public void TestInvalidEncoderInput() - { - //Null coordinates - Assert.Throws(() => - { - PolylineEncoderDecoder.Decode(null); - }); - - - //Empty coordinates list test - Assert.Throws(() => - { - PolylineEncoderDecoder.Decode(string.Empty); - }); - } - [Test] - public void TestThirdDimension() - { - Assert.IsTrue(PolylineEncoderDecoder.GetThirdDimension("BFoz5xJ67i1BU") == ThirdDimension.Absent); - Assert.IsTrue(PolylineEncoderDecoder.GetThirdDimension("BVoz5xJ67i1BU") == ThirdDimension.Level); - Assert.IsTrue(PolylineEncoderDecoder.GetThirdDimension("BlBoz5xJ67i1BU") == ThirdDimension.Altitude); - Assert.IsTrue(PolylineEncoderDecoder.GetThirdDimension("B1Boz5xJ67i1BU") == ThirdDimension.Elevation); - } - - [Test] - public void TestDecodeConvertValue() - { - var encoded = "h_wqiB"; - var expected = -179.98321; - var index = 0; - var computed = 0d; - var conv = new PolylineEncoderDecoder.Converter(5); - conv.DecodeValue(encoded.ToCharArray(), ref index, ref computed); - Assert.AreEqual(computed, expected); - } - - [Test] - public void TestSimpleLatLngDecoding() - { - var computed = PolylineEncoderDecoder.Decode("BFoz5xJ67i1B1B7PzIhaxL7Y"); - var expected = new List(); - expected.Add(new LatLngZ(50.10228, 8.69821)); - expected.Add(new LatLngZ(50.10201, 8.69567)); - expected.Add(new LatLngZ(50.10063, 8.69150)); - expected.Add(new LatLngZ(50.09878, 8.68752)); - - Assert.AreEqual(computed.Count, expected.Count); - for (int i = 0; i < computed.Count; ++i) - { - Assert.AreEqual(computed[i], expected[i]); - } - } - - [Test] - public void TestComplexLatLngDecoding() - { - var computed = PolylineEncoderDecoder.Decode("BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e"); - - var pairs = new List(); - pairs.Add(new LatLngZ(52.51994, 13.38663)); - pairs.Add(new LatLngZ(52.51009, 13.28169)); - pairs.Add(new LatLngZ(52.43518, 13.19352)); - pairs.Add(new LatLngZ(52.41073, 13.19645)); - pairs.Add(new LatLngZ(52.38871, 13.15578)); - pairs.Add(new LatLngZ(52.37278, 13.14910)); - pairs.Add(new LatLngZ(52.37375, 13.11546)); - pairs.Add(new LatLngZ(52.38752, 13.08722)); - pairs.Add(new LatLngZ(52.40294, 13.07062)); - pairs.Add(new LatLngZ(52.41058, 13.07555)); - - Assert.AreEqual(computed.Count, pairs.Count); - for (int i = 0; i < computed.Count; ++i) - { - Assert.AreEqual(computed[i], pairs[i]); - } - } - - [Test] - public void TestLatLngZDecode() - { - var computed = PolylineEncoderDecoder.Decode("BlBoz5xJ67i1BU1B7PUzIhaUxL7YU"); - var tuples = new List(); - - tuples.Add(new LatLngZ(50.10228, 8.69821, 10)); - tuples.Add(new LatLngZ(50.10201, 8.69567, 20)); - tuples.Add(new LatLngZ(50.10063, 8.69150, 30)); - tuples.Add(new LatLngZ(50.09878, 8.68752, 40)); - - Assert.AreEqual(computed.Count, tuples.Count); - for (int i = 0; i < computed.Count; ++i) - { - Assert.AreEqual(computed[i], tuples[i]); - } - } - - - private static readonly Regex EncodedResultRegex = new Regex(@"^\{\((?:(?\d+)(?:, ?)?)+\); \[(?\((?:(?:-?\d+\.\d+)(?:, ?)?){2,3}\)(?:, ?)?)+\]\}", RegexOptions.Compiled); - private static readonly Regex EncodedResultCoordinatesRegex = new Regex(@"^\((?:(?-?\d+\.\d+)(?:, ?)?){2,3}\)(?:, ?)?", RegexOptions.Compiled); - private ExpectedResult ParseExpectedTestResult(string encodedResult) - { - var resultMatch = EncodedResultRegex.Match(encodedResult); - if (!resultMatch.Success) - throw new ArgumentException("encodedResult"); - - Precision precision; - if (resultMatch.Groups["prec"].Captures.Count == 1) - { - precision = new Precision(int.Parse(resultMatch.Groups["prec"].Captures[0].Value)); - } - else if (resultMatch.Groups["prec"].Captures.Count == 3) - { - precision = new Precision( - int.Parse(resultMatch.Groups["prec"].Captures[0].Value), - int.Parse(resultMatch.Groups["prec"].Captures[1].Value), - (ThirdDimension)int.Parse(resultMatch.Groups["prec"].Captures[2].Value)); - } - else - { - throw new ArgumentException("encodedResult"); - } - - var coordinates = new List(); - foreach (Capture latlngzCapture in resultMatch.Groups["latlngz"].Captures) - { - LatLngZ coordinate; - var coordinateMatch = EncodedResultCoordinatesRegex.Match(latlngzCapture.Value); - if (coordinateMatch.Groups["number"].Captures.Count == 2) - { - coordinate = new LatLngZ( - double.Parse(coordinateMatch.Groups["number"].Captures[0].Value), - double.Parse(coordinateMatch.Groups["number"].Captures[1].Value)); - } - else if (coordinateMatch.Groups["number"].Captures.Count == 3) - { - coordinate = new LatLngZ( - double.Parse(coordinateMatch.Groups["number"].Captures[0].Value), - double.Parse(coordinateMatch.Groups["number"].Captures[1].Value), - double.Parse(coordinateMatch.Groups["number"].Captures[2].Value)); - } - else - { - throw new ArgumentException("latlngz"); - } - - coordinates.Add(coordinate); - } - - return new ExpectedResult(precision, coordinates); - } - - private class Precision - { - public int Precision2d { get; } - public int Precision3d { get; } - public ThirdDimension Type3d { get; } - - public Precision(int precision2d, int precision3d = 0, ThirdDimension type3d = ThirdDimension.Absent) - { - Precision2d = precision2d; - Precision3d = precision3d; - Type3d = type3d; - } - } - - private class ExpectedResult - { - public Precision Precision { get; } - - public List Coordinates { get; } - - public ExpectedResult(Precision precision, List coordinates) - { - Precision = precision; - Coordinates = coordinates; - } - } - } -} diff --git a/golang/flexpolyline/data.go b/golang/flexpolyline/data.go deleted file mode 100644 index 5fc29f8..0000000 --- a/golang/flexpolyline/data.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (C) 2019 HERE Europe B.V. -// Licensed under MIT, see full license in LICENSE -// SPDX-License-Identifier: MIT -// License-Filename: LICENSE - -// Package flexpolyline contains tools to encode and decode FlexPolylines -// This file defines data structures to store FlexPolylines - -package flexpolyline - -import ( - "fmt" - "math" -) - -// FlexPolyline specification version -const FormatVersion uint = 1 - -// Number of decimal digits after the comma -type Precision uint8 - -func (p Precision) factor() float64 { - return math.Pow10(int(p)) -} - -// Whether the third dimension is present and what meaning it has -type Type3D uint8 - -const ( - Absent Type3D = iota - Level - Altitude - Elevation - Reserved1 - Reserved2 - Custom1 - Custom2 -) - -// A point on the Earth surface with an optional third dimension -type Point struct { - Lat float64 - Lng float64 - ThirdDim float64 -} - -// Structure to store FlexPolyline -type Polyline struct { - coordinates []Point - precision2D Precision - precision3D Precision - type3D Type3D -} - -func (p* Polyline) Coordinates() []Point { - return p.coordinates -} - -func (p* Polyline) Precision2D() Precision { - return p.precision2D -} - -func (p* Polyline) Precision3D() Precision { - return p.precision3D -} - -func (p* Polyline) Type3D() Type3D { - return p.type3D -} - -// Creates a two dimensional FlexPolyline -func CreatePolyline(precision Precision, points []Point) (*Polyline, error) { - err := checkArgs(Absent, precision, 0) - if err != nil { - return nil, err - } - return &Polyline{ - coordinates: points, - precision2D: precision, - }, nil -} - -// Creates a two dimensional FlexPolyline. Panics if arguments are bad. -func MustCreatePolyline(precision Precision, points []Point) *Polyline { - p, err := CreatePolyline(precision, points) - if err != nil { - panic(err) - } - return p -} - -// Creates a three dimensional FlexPolyline -func CreatePolyline3D(type3D Type3D, precision2D, precision3D Precision, points []Point) (*Polyline, error) { - err := checkArgs(type3D, precision2D, precision3D) - if err != nil { - return nil, err - } - return &Polyline{ - coordinates: points, - precision2D: precision2D, - precision3D: precision3D, - type3D: type3D, - }, nil -} - -// Creates a three dimensional FlexPolyline. Panics if arguments are bad. -func MustCreatePolyline3D(type3D Type3D, precision2D, precision3D Precision, points []Point) *Polyline { - p, err := CreatePolyline3D(type3D, precision2D, precision3D, points) - if err != nil { - panic(err) - } - return p -} - -// Encodes a Polyline to a string -func (p *Polyline) Encode() (string, error) { - return Encode(p) -} - -func circaCharsPerPoint(header *Polyline) int { - const assumedMaxThirdDimValue = 10000. - circaNumChars := 2.*math.Log(360.*header.Precision2D().factor())/math.Log(64) - if header.Type3D() != Absent { - circaNumChars += math.Log(assumedMaxThirdDimValue*header.Precision3D().factor())/math.Log(64) - } - return int(math.Ceil(circaNumChars)) -} - - -func checkArgs(type3D Type3D, precision2D, precision3D Precision) error { - if precision2D > 15 { - return fmt.Errorf("Precision2D %d > max Precision2D (15)", precision2D) - } - if type3D > Custom2 { - return fmt.Errorf("Type3D %d > max Type3D (7)", type3D) - } - if type3D == Reserved1 || type3D == Reserved2 { - return fmt.Errorf("Type3D %d reserved for future use", type3D) - } - if precision3D > 15 { - return fmt.Errorf("Precision3D %d > max Precision3D (15)", precision3D) - } - return nil -} diff --git a/golang/flexpolyline/data_test.go b/golang/flexpolyline/data_test.go deleted file mode 100644 index 7b3ac3d..0000000 --- a/golang/flexpolyline/data_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) 2019 HERE Europe B.V. -// Licensed under MIT, see full license in LICENSE -// SPDX-License-Identifier: MIT -// License-Filename: LICENSE - -package flexpolyline - -import "testing" - -func TestPrecisionIsChecked(t *testing.T) { - for i := 0; i <= 15; i++ { - _, err := CreatePolyline(Precision(i), emptyPolyline) - if err != nil { - t.Error("Precision2D in allowed range, but returns an error") - } - } - _, err := CreatePolyline(16, emptyPolyline) - if err == nil { - t.Error("Precision2D too high, but does not return an error") - } -} - -func TestThirdDimensionFlagIsChecked(t *testing.T) { - for i := 0; i <= 3; i++ { - _, err := CreatePolyline3D(Type3D(i), 5, 5, emptyPolyline) - if err != nil { - t.Error("Third dimension flag in allowed range, but returns an error") - } - } - for i := 4; i <= 5; i++ { - _, err := CreatePolyline3D(Type3D(i), 5, 5, emptyPolyline) - if err == nil { - t.Error("Third dimension flag has reserved value, but does not return an error") - } - } - for i := 6; i <= 7; i++ { - _, err := CreatePolyline3D(Type3D(i), 5, 5, emptyPolyline) - if err != nil { - t.Error("Third dimension flag in allowed range, but returns an error") - } - } - _, err := CreatePolyline3D(8, 5, 5, emptyPolyline) - if err == nil { - t.Error("Third dimension flag too big, but does not return an error") - } -} - -func TestThirdDimensionPrecisionIsChecked(t *testing.T) { - for i := 0; i <= 15; i++ { - _, err := CreatePolyline3D(0, 5, Precision(i), emptyPolyline) - if err != nil { - t.Error("Third dimension precision in allowed range, but returns an error") - } - } - _, err := CreatePolyline3D(0, 5, 16, emptyPolyline) - if err == nil { - t.Error("Third dimension precision too high, but does not return an error") - } -} - diff --git a/golang/flexpolyline/decode.go b/golang/flexpolyline/decode.go deleted file mode 100644 index d532d2e..0000000 --- a/golang/flexpolyline/decode.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (C) 2019 HERE Europe B.V. -// Licensed under MIT, see full license in LICENSE -// SPDX-License-Identifier: MIT -// License-Filename: LICENSE - -// Package flexpolyline contains tools to encode and decode FlexPolylines -// This file defines the Decode() function - -package flexpolyline - -import ( - "fmt" -) - -// Decodes a Polyline from a string following the format specified in -// https://github.com/heremaps/flexible-polyline/blob/master/README.md -func Decode(polyline string) (*Polyline, error) { - header, body, err := decodeHeader(polyline) - if err != nil { - return nil, err - } - multiplierDegree := header.Precision2D().factor() - multiplierZ := header.Precision3D().factor() - var lastLat, lastLng, lastZ float64 - circaNumChars := len(body)/circaCharsPerPoint(header) + 1 - result := make([]Point, 0, circaNumChars) - for line := body; len(line) > 0; { - nextIntValue, polylineTail, err := decodeValueAndAdvance(line) - if err != nil { - return nil, err - } - - deltaLat := float64(toSigned(nextIntValue)) / multiplierDegree - lastLat += deltaLat - - nextIntValue, polylineTail, err = decodeValueAndAdvance(polylineTail) - if err != nil { - return nil, err - } - deltaLng := float64(toSigned(nextIntValue)) / multiplierDegree - lastLng += deltaLng - nextPoint := Point{Lat: lastLat, Lng: lastLng} - if header.Type3D() != Absent { - nextIntValue, polylineTail, err = decodeValueAndAdvance(polylineTail) - if err != nil { - return nil, err - } - deltaZ := float64(toSigned(nextIntValue)) / multiplierZ - lastZ += deltaZ - nextPoint.ThirdDim = lastZ - } - result = append(result, nextPoint) - - line = polylineTail - } - - if header.Type3D() != Absent { - return MustCreatePolyline3D(header.Type3D(), header.Precision2D(), header.Precision3D(), result), nil - } else { - return MustCreatePolyline(header.Precision2D(), result), nil - } -} - -// Returns the type of the third dimension data, parsing only the header -func GetThirdDimension(polyline string) (Type3D, error) { - header, _, err := decodeHeader(polyline) - if err != nil { - return 0, err - } - if header == nil { - return 0, fmt.Errorf("received nil header") - } - return header.Type3D(), nil -} - -var decodingTable = [...]int8 { - 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51} - -func decodeChar(char byte) (int8, error) { - charValue := uint8(char) - value := decodingTable[charValue-45] - if value < 0 { - return 0, fmt.Errorf("invalid encoding") - } - return value, nil -} - -func toSigned(value int64) int64 { - if value & 1 != 0 { - value = ^value - } - value >>= 1 - return value -} - -func decodeHeader(polyline string) (*Polyline, string, error) { - version, err := decodeChar(polyline[0]) - if err != nil { - return nil, "", err - } - if uint(version) != FormatVersion { - return nil, "", fmt.Errorf("Invalid format version %d, can only handle %d", version, FormatVersion) - } - headerContent, body, err := decodeValueAndAdvance(polyline[1:]) - if err != nil { - return nil, "", err - } - - precision2D := Precision(headerContent & 15) - headerContent >>= 4 - type3D := Type3D(headerContent & 7) - precision3D := Precision(headerContent >> 3) - - return &Polyline{ - coordinates: nil, - precision2D: precision2D, - precision3D: precision3D, - type3D: type3D, - }, body, nil -} - -func decodeValueAndAdvance(polyline string) (int64, string, error) { - var ( - result int64 - shift uint8 - ) - for i := 0; i < len(polyline); i++ { - char := polyline[i] - value, err := decodeChar(char) - if err != nil { - return 0, "", err - } - orValue := int64(value & 0x1F) << shift - result |= orValue - if (value & 0x20) == 0 { - return result, polyline[i+1:], nil - } else { - shift += 5 - } - } - return result, "", nil -} diff --git a/golang/flexpolyline/decode_test.go b/golang/flexpolyline/decode_test.go deleted file mode 100644 index 3cf752d..0000000 --- a/golang/flexpolyline/decode_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (C) 2019 HERE Europe B.V. -// Licensed under MIT, see full license in LICENSE -// SPDX-License-Identifier: MIT -// License-Filename: LICENSE - -package flexpolyline - -import ( - "math" - "testing" -) - -const testDataPrecision = 7 -var testData2D = []Point{ - {Lat: 50.10228, Lng: 8.69821}, - {Lat: 50.10201, Lng: 8.69567}, - {Lat: 50.10063, Lng: 8.69150}, - {Lat: 50.09878, Lng: 8.68752}, -} -var testData3D = []Point { - {Lat: 50.10228, Lng: 8.69821, ThirdDim: 10}, - {Lat: 50.10201, Lng: 8.69567, ThirdDim: 20}, - {Lat: 50.10063, Lng: 8.69150, ThirdDim: 30}, - {Lat: 50.09878, Lng: 8.68752, ThirdDim: 40}, -} - -func arePointsEqualToPrecision(actual, expected []Point, precision Precision) bool { - if len(actual) != len(expected) {return false} - precisionMultiplier := precision.factor() - for i := 0; i < len(actual); i++ { - roundedLatActual := int64(math.Round(actual[i].Lat * precisionMultiplier)) - roundedLatExpected := int64(math.Round(expected[i].Lat * precisionMultiplier)) - roundedLngActual := int64(math.Round(actual[i].Lng * precisionMultiplier)) - roundedLngExpected := int64(math.Round(expected[i].Lng * precisionMultiplier)) - rounded3rdActual := int64(math.Round(actual[i].ThirdDim * precisionMultiplier)) - rounded3rdExpected := int64(math.Round(expected[i].ThirdDim * precisionMultiplier)) - if roundedLatActual != roundedLatExpected || roundedLngActual != roundedLngExpected || rounded3rdActual != rounded3rdExpected{ - return false - } - } - return true -} - -func TestDecodeStaticHeader(t *testing.T) { - polyline := "BFoz5xJ67i1B1B7PzIhaxL7Y" - result, _, err := decodeHeader(polyline) - if err != nil { - t.Errorf("Decode returned error %s", err) - } - if result.Precision2D() != 5 || result.Type3D() != 0 || result.Precision3D() != 0 { - t.Errorf("Decode returned unexpected header %v", result) - } -} - -func TestDecodeStaticHeaderWithAltitude(t *testing.T) { - polyline := "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU" - result, _, err := decodeHeader(polyline) - if err != nil { - t.Errorf("Decode returned error %s", err) - } - if result.Type3D() != Altitude || result.Precision3D() != 0 { - t.Errorf("Decode returned unexpected header %v", result) - } -} - -func TestDecodeValue(t *testing.T) { - polyline := "BFoz5xJ67i1B1B7PzIhaxL7Y" - result, err := Decode(polyline) - if err != nil { - t.Errorf("Decode returned error %s", err) - } - if !arePointsEqualToPrecision(result.Coordinates(), testData2D, testDataPrecision) { - t.Errorf("Expected: %v, got: %v", testData2D, result) - } -} - -func TestDecodeValueWithAltitude(t *testing.T) { - polyline := "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU" - result, err := Decode(polyline) - if err != nil { - t.Errorf("Decode returned error %s", err) - } - if !arePointsEqualToPrecision(result.Coordinates(), testData3D, 7) { - t.Errorf("Expected: %v, got: %v", testData3D, result) - } -} - -func TestDecodeValueEncodedWithDifferent3rdDimPrecisions(t *testing.T) { - for i := 0.; i <= 15.; i++ { - toEncode := MustCreatePolyline3D(Altitude, 5, Precision(i), testData3D) - polyline, _ := Encode(toEncode) - result, err := Decode(polyline) - if err != nil { - t.Errorf("Decode returned error %s", err) - } - comparisonPrecision := Precision(math.Min(i, testDataPrecision)) - if !arePointsEqualToPrecision(result.Coordinates(), testData3D, comparisonPrecision) { - t.Errorf("Precision2D: %f, Expected: %v, got: %v", i, testData3D, result) - } - } -} - -func TestThirdDimensionFlagWithDifferentValues(t *testing.T) { - flags := []Type3D{Absent, Level, Altitude, Elevation, Custom1, Custom2} - for i := 0; i < len(flags); i++ { - toEncode := MustCreatePolyline3D(flags[i], 5, 0, []Point{{Lat: 0, Lng: 0}}) - polyline, _ := Encode(toEncode) - thirdDimensionFlag, err := GetThirdDimension(polyline) - if err != nil { - t.Errorf("GetThirdDimension returned error %s", err) - } - if thirdDimensionFlag != flags[i] { - t.Errorf("%d Expected: %d, got: %d", i, flags[i], thirdDimensionFlag) - } - } -} diff --git a/golang/flexpolyline/encode.go b/golang/flexpolyline/encode.go deleted file mode 100644 index 172752f..0000000 --- a/golang/flexpolyline/encode.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2019 HERE Europe B.V. -// Licensed under MIT, see full license in LICENSE -// SPDX-License-Identifier: MIT -// License-Filename: LICENSE - -// Package flexpolyline contains tools to encode and decode FlexPolyline -// This file defines the Encode() function - -package flexpolyline - -import ( - "math" - "strings" -) - -// Encodes a Polyline to a string following the format specified in -// https://github.com/heremaps/flexible-polyline/blob/master/README.md -func Encode(polyline *Polyline) (string, error) { - multiplierDegree := polyline.Precision2D().factor() - multiplierZ := polyline.Precision3D().factor() - - var lastLat, lastLng, lastZ int64 - var builder strings.Builder - builder.Grow(circaCharsPerPoint(polyline)*len(polyline.Coordinates())+4) - encodeHeader(polyline, &builder) - - for i := 0; i < len(polyline.Coordinates()); i++ { - location := polyline.Coordinates()[i] - lat := int64(math.Round(location.Lat * multiplierDegree)) - encodeScaledValue(lat - lastLat, &builder) - lastLat = lat - - lng := int64(math.Round(location.Lng * multiplierDegree)) - encodeScaledValue(lng - lastLng, &builder) - lastLng = lng - - if polyline.Type3D() != Absent { - z := int64(math.Round(location.ThirdDim * multiplierZ)) - encodeScaledValue(z - lastZ, &builder) - lastZ = z - } - - } - return builder.String(), nil -} - -const encodingTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" - -func encodeUint(value uint64, builder *strings.Builder) { - for ; value > 0x1F; { - pos := (value & 0x1F) | 0x20 - builder.WriteString(string(encodingTable[pos])) - value >>= 5 - } - builder.WriteString(string(encodingTable[value])) -} - -func encodeScaledValue(value int64, builder* strings.Builder) { - uintValue := uint64(value) - uintValue <<= 1 - if value < 0 { - uintValue = ^uintValue - } - encodeUint(uintValue, builder) -} - -func encodeHeader(polyline *Polyline, builder *strings.Builder) { - headerContent := (uint64(polyline.Precision3D()) << 7) | (uint64(polyline.Type3D()) << 4) | uint64(polyline.Precision2D()) - encodeUint(uint64(FormatVersion), builder) - encodeUint(headerContent, builder) -} diff --git a/golang/flexpolyline/encode_test.go b/golang/flexpolyline/encode_test.go deleted file mode 100644 index 8d3a214..0000000 --- a/golang/flexpolyline/encode_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (C) 2019 HERE Europe B.V. -// Licensed under MIT, see full license in LICENSE -// SPDX-License-Identifier: MIT -// License-Filename: LICENSE - -package flexpolyline - -import ( - "math/rand" - "testing" -) - - -var emptyPolyline = []Point{{Lat: 0, Lng: 0}} - -var toEncode2D = []Point{ - {Lat: 50.1022829, Lng: 8.6982122}, - {Lat: 50.1020076, Lng: 8.6956695}, - {Lat: 50.1006313, Lng: 8.6914960}, - {Lat: 50.0987800, Lng: 8.6875156}, -} - -var toEncode3D = []Point{ - {Lat: 50.1022829, Lng: 8.6982122, ThirdDim: 10}, - {Lat: 50.1020076, Lng: 8.6956695, ThirdDim: 20}, - {Lat: 50.1006313, Lng: 8.6914960, ThirdDim: 30}, - {Lat: 50.0987800, Lng: 8.6875156, ThirdDim: 40}, -} - -var toEncodeLong = []Point{ - {Lat: 52.5199356, Lng: 13.3866272}, - {Lat: 52.5100899, Lng: 13.2816896}, - {Lat: 52.4351807, Lng: 13.1935196}, - {Lat: 52.4107285, Lng: 13.1964502}, - {Lat: 52.38871, Lng: 13.1557798}, - {Lat: 52.3727798, Lng: 13.1491003}, - {Lat: 52.3737488, Lng: 13.1154604}, - {Lat: 52.3875198, Lng: 13.0872202}, - {Lat: 52.4029388, Lng: 13.0706196}, - {Lat: 52.4105797, Lng: 13.0755529}, -} - - -func testEncoding(t *testing.T, expected string, precision Precision, thirdDimensionFlag Type3D, thirdDimensionPrecision Precision, pointsToEncode []Point) { - toEncode := MustCreatePolyline3D(thirdDimensionFlag, precision, thirdDimensionPrecision, pointsToEncode) - result, err := Encode(toEncode) - checkResult(t, result, expected, err) -} - -func checkResult(t *testing.T, expected string, result string, err error) { - if err != nil { - t.Errorf("Encode returned error %s", err) - } - if result != expected { - t.Errorf("Expected: %s, got: %s", expected, result) - } -} - -func TestEncodeValue(t *testing.T) { - expected := "BFoz5xJ67i1B1B7PzIhaxL7Y" - testEncoding(t, expected, 5, Absent, 0, toEncode2D) -} - -func TestAltitude(t *testing.T) { - expected := "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU" - testEncoding(t, expected, 5, Altitude, 0, toEncode3D) -} - -func TestLongerPolyline(t *testing.T) { - expected := "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e" - testEncoding(t, expected, 5, Absent, 0, toEncodeLong) -} - -func TestAsMethod(t *testing.T) { - expected := "BFoz5xJ67i1B1B7PzIhaxL7Y" - result, err := MustCreatePolyline(5, toEncode2D).Encode() - checkResult(t, result, expected, err) - expected = "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU" - result, err = MustCreatePolyline3D(Altitude, 5, 0, toEncode3D).Encode() - checkResult(t, result, expected, err) - expected = "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e" - result, err = MustCreatePolyline(5, toEncodeLong).Encode() - checkResult(t, result, expected, err) -} - -func BenchmarkEncode(b *testing.B) { - points := buildPoints(b.N) - polyline := MustCreatePolyline(4, points) - - b.ResetTimer() - _, err := Encode(polyline) - if err != nil { - b.Fail() - } -} - -func buildPoints(n int) []Point { - base := Point{Lat: 50, Lng: 8} - - points := make([]Point, 0, n) - for i := 0; i < n; i++ { - points = append(points, Point{ - Lat: base.Lat + 10*rand.Float64(), - Lng: base.Lat + 10*rand.Float64(), - }) - } - - return points -} diff --git a/java/pom.xml b/java/pom.xml deleted file mode 100644 index 16b795e..0000000 --- a/java/pom.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - 4.0.0 - Flexible Polyline - com.here - flexpolyline - 0.1.0 - pom - Flexible Polyline encoding: a lossy compressed representation of a list of coordinate pairs or triples - https://github.com/heremaps/flexible-polyline - - - - MIT - https://opensource.org/licenses/MIT - repo - - - - - scm:git:https://github.com/heremaps/flexible-polyline.git - scm:git:git@github.com:heremaps/flexible-polyline.git - https://github.com/heremaps/flexible-polyline - - diff --git a/java/src/com/here/flexpolyline/PolylineEncoderDecoder.java b/java/src/com/here/flexpolyline/PolylineEncoderDecoder.java deleted file mode 100644 index ba064d7..0000000 --- a/java/src/com/here/flexpolyline/PolylineEncoderDecoder.java +++ /dev/null @@ -1,439 +0,0 @@ -/* - * Copyright (C) 2019 HERE Europe B.V. - * Licensed under MIT, see full license in LICENSE - * SPDX-License-Identifier: MIT - * License-Filename: LICENSE - */ -package com.here.flexpolyline; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -/** - * The polyline encoding is a lossy compressed representation of a list of coordinate pairs or coordinate triples. - * It achieves that by: - *

    - *
  1. Reducing the decimal digits of each value. - *
  2. Encoding only the offset from the previous point. - *
  3. Using variable length for each coordinate delta. - *
  4. Using 64 URL-safe characters to display the result. - *

- * - * The advantage of this encoding are the following: - *

    - *
  • Output string is composed by only URL-safe characters - *
  • Floating point precision is configurable - *
  • It allows to encode a 3rd dimension with a given precision, which may be a level, altitude, elevation or some other custom value - *

- */ -public class PolylineEncoderDecoder { - - /** - * Header version - * A change in the version may affect the logic to encode and decode the rest of the header and data - */ - public static final byte FORMAT_VERSION = 1; - - //Base64 URL-safe characters - public static final char[] ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray(); - - public static final int[] DECODING_TABLE = { - 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 - }; - /** - * Encode the list of coordinate triples.

- * The third dimension value will be eligible for encoding only when ThirdDimension is other than ABSENT. - * This is lossy compression based on precision accuracy. - * - * @param coordinates {@link List} of coordinate triples that to be encoded. - * @param precision Floating point precision of the coordinate to be encoded. - * @param thirdDimension {@link ThirdDimension} which may be a level, altitude, elevation or some other custom value - * @param thirdDimPrecision Floating point precision for thirdDimension value - * @return URL-safe encoded {@link String} for the given coordinates. - */ - public static String encode(List coordinates, int precision, ThirdDimension thirdDimension, int thirdDimPrecision) { - if (coordinates == null || coordinates.size() == 0) { - throw new IllegalArgumentException("Invalid coordinates!"); - } - if (thirdDimension == null) { - throw new IllegalArgumentException("Invalid thirdDimension"); - } - Encoder enc = new Encoder(precision, thirdDimension, thirdDimPrecision); - Iterator iter = coordinates.iterator(); - while (iter.hasNext()) { - enc.add(iter.next()); - } - return enc.getEncoded(); - } - - /** - * Decode the encoded input {@link String} to {@link List} of coordinate triples.

- * @param encoded URL-safe encoded {@link String} - * @return {@link List} of coordinate triples that are decoded from input - * - * @see PolylineDecoder#getThirdDimension(String) getThirdDimension - * @see LatLngZ - */ - public static final List decode(String encoded) { - - if (encoded == null || encoded.trim().isEmpty()) { - throw new IllegalArgumentException("Invalid argument!"); - } - List result = new ArrayList<>(); - Decoder dec = new Decoder(encoded); - AtomicReference lat = new AtomicReference<>(0d); - AtomicReference lng = new AtomicReference<>(0d); - AtomicReference z = new AtomicReference<>(0d); - - while (dec.decodeOne(lat, lng, z)) { - result.add(new LatLngZ(lat.get(), lng.get(), z.get())); - lat = new AtomicReference<>(0d); - lng = new AtomicReference<>(0d); - z = new AtomicReference<>(0d); - } - return result; - } - - /** - * ThirdDimension type from the encoded input {@link String} - * @param encoded URL-safe encoded coordinate triples {@link String} - * @return type of {@link ThirdDimension} - */ - public static ThirdDimension getThirdDimension(String encoded) { - AtomicInteger index = new AtomicInteger(0); - AtomicLong header = new AtomicLong(0); - Decoder.decodeHeaderFromString(encoded, index, header); - return ThirdDimension.fromNum((header.get() >> 4) & 7); - } - - public byte getVersion() { - return FORMAT_VERSION; - } - - /* - * Single instance for configuration, validation and encoding for an input request. - */ - private static class Encoder { - - private final StringBuilder result; - private final Converter latConveter; - private final Converter lngConveter; - private final Converter zConveter; - private final ThirdDimension thirdDimension; - - public Encoder(int precision, ThirdDimension thirdDimension, int thirdDimPrecision) { - this.latConveter = new Converter(precision); - this.lngConveter = new Converter(precision); - this.zConveter = new Converter(thirdDimPrecision); - this.thirdDimension = thirdDimension; - this.result = new StringBuilder(); - encodeHeader(precision, this.thirdDimension.getNum(), thirdDimPrecision); - } - - private void encodeHeader(int precision, int thirdDimensionValue, int thirdDimPrecision) { - /* - * Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char - */ - if (precision < 0 || precision > 15) { - throw new IllegalArgumentException("precision out of range"); - } - - if (thirdDimPrecision < 0 || thirdDimPrecision > 15) { - throw new IllegalArgumentException("thirdDimPrecision out of range"); - } - - if (thirdDimensionValue < 0 || thirdDimensionValue > 7) { - throw new IllegalArgumentException("thirdDimensionValue out of range"); - } - long res = (thirdDimPrecision << 7) | (thirdDimensionValue << 4) | precision; - Converter.encodeUnsignedVarint(PolylineEncoderDecoder.FORMAT_VERSION, result); - Converter.encodeUnsignedVarint(res, result); - } - - private void add(double lat, double lng) { - latConveter.encodeValue(lat, result); - lngConveter.encodeValue(lng, result); - } - - private void add(double lat, double lng, double z) { - add(lat, lng); - if (this.thirdDimension != ThirdDimension.ABSENT) { - zConveter.encodeValue(z, result); - } - } - - private void add(LatLngZ tuple) { - if(tuple == null) { - throw new IllegalArgumentException("Invalid LatLngZ tuple"); - } - add(tuple.lat, tuple.lng, tuple.z); - } - - private String getEncoded() { - return this.result.toString(); - } - } - - /* - * Single instance for decoding an input request. - */ - private static class Decoder { - - private final String encoded; - private final AtomicInteger index; - private final Converter latConveter; - private final Converter lngConveter; - private final Converter zConveter; - - private int precision; - private int thirdDimPrecision; - private ThirdDimension thirdDimension; - - - public Decoder(String encoded) { - this.encoded = encoded; - this.index = new AtomicInteger(0); - decodeHeader(); - this.latConveter = new Converter(precision); - this.lngConveter = new Converter(precision); - this.zConveter = new Converter(thirdDimPrecision); - } - - private boolean hasThirdDimension() { - return thirdDimension != ThirdDimension.ABSENT; - } - - private void decodeHeader() { - AtomicLong header = new AtomicLong(0); - decodeHeaderFromString(encoded, index, header); - precision = (int) (header.get() & 15); // we pick the first 4 bits only - header.set(header.get() >> 4); - thirdDimension = ThirdDimension.fromNum(header.get() & 7); // we pick the first 3 bits only - thirdDimPrecision = (int) ((header.get() >> 3) & 15); - } - - private static void decodeHeaderFromString(String encoded, AtomicInteger index, AtomicLong header) { - AtomicLong value = new AtomicLong(0); - - // Decode the header version - if(!Converter.decodeUnsignedVarint(encoded.toCharArray(), index, value)) { - throw new IllegalArgumentException("Invalid encoding"); - } - if (value.get() != FORMAT_VERSION) { - throw new IllegalArgumentException("Invalid format version"); - } - // Decode the polyline header - if(!Converter.decodeUnsignedVarint(encoded.toCharArray(), index, value)) { - throw new IllegalArgumentException("Invalid encoding"); - } - header.set(value.get()); - } - - - private boolean decodeOne(AtomicReference lat, - AtomicReference lng, - AtomicReference z) { - if (index.get() == encoded.length()) { - return false; - } - if (!latConveter.decodeValue(encoded, index, lat)) { - throw new IllegalArgumentException("Invalid encoding"); - } - if (!lngConveter.decodeValue(encoded, index, lng)) { - throw new IllegalArgumentException("Invalid encoding"); - } - if (hasThirdDimension()) { - if (!zConveter.decodeValue(encoded, index, z)) { - throw new IllegalArgumentException("Invalid encoding"); - } - } - return true; - } - } - - //Decode a single char to the corresponding value - private static int decodeChar(char charValue) { - int pos = charValue - 45; - if (pos < 0 || pos > 77) { - return -1; - } - return DECODING_TABLE[pos]; - } - - /* - * Stateful instance for encoding and decoding on a sequence of Coordinates part of an request. - * Instance should be specific to type of coordinates (e.g. Lat, Lng) - * so that specific type delta is computed for encoding. - * Lat0 Lng0 3rd0 (Lat1-Lat0) (Lng1-Lng0) (3rdDim1-3rdDim0) - */ - public static class Converter { - - private long multiplier = 0; - private long lastValue = 0; - - public Converter(int precision) { - setPrecision(precision); - } - - private void setPrecision(int precision) { - multiplier = (long) Math.pow(10, Double.valueOf(precision)); - } - - private static void encodeUnsignedVarint(long value, StringBuilder result) { - while (value > 0x1F) { - byte pos = (byte) ((value & 0x1F) | 0x20); - result.append(ENCODING_TABLE[pos]); - value >>= 5; - } - result.append(ENCODING_TABLE[(byte) value]); - } - - void encodeValue(double value, StringBuilder result) { - /* - * Round-half-up - * round(-1.4) --> -1 - * round(-1.5) --> -2 - * round(-2.5) --> -3 - */ - long scaledValue = (long) Math.round(Math.abs(value * multiplier)) * Math.round(Math.signum(value)); - long delta = scaledValue - lastValue; - boolean negative = delta < 0; - - lastValue = scaledValue; - - // make room on lowest bit - delta <<= 1; - - // invert bits if the value is negative - if (negative) { - delta = ~delta; - } - encodeUnsignedVarint(delta, result); - } - - private static boolean decodeUnsignedVarint(char[] encoded, - AtomicInteger index, - AtomicLong result) { - short shift = 0; - long delta = 0; - long value; - - while (index.get() < encoded.length) { - value = decodeChar(encoded[index.get()]); - if (value < 0) { - return false; - } - index.incrementAndGet(); - delta |= (value & 0x1F) << shift; - if ((value & 0x20) == 0) { - result.set(delta); - return true; - } else { - shift += 5; - } - } - - if (shift > 0) { - return false; - } - return true; - } - - //Decode single coordinate (say lat|lng|z) starting at index - boolean decodeValue(String encoded, - AtomicInteger index, - AtomicReference coordinate) { - AtomicLong delta = new AtomicLong(); - if (!decodeUnsignedVarint(encoded.toCharArray(), index, delta)) { - return false; - } - if ((delta.get() & 1) != 0) { - delta.set(~delta.get()); - } - delta.set(delta.get()>>1); - lastValue += delta.get(); - coordinate.set(((double)lastValue / multiplier)); - return true; - } - } - - /** - * 3rd dimension specification. - * Example a level, altitude, elevation or some other custom value. - * ABSENT is default when there is no third dimension en/decoding required. - */ - public static enum ThirdDimension { - ABSENT(0), - LEVEL(1), - ALTITUDE(2), - ELEVATION(3), - RESERVED1(4), - RESERVED2(5), - CUSTOM1(6), - CUSTOM2(7); - - private int num; - - ThirdDimension(int num) { - this.num = num; - } - - public int getNum() { - return num; - } - - public static ThirdDimension fromNum(long value) { - for (ThirdDimension dim : ThirdDimension.values()) { - if (dim.getNum() == value) { - return dim; - } - } - return null; - } - } - - /** - * Coordinate triple - */ - public static class LatLngZ { - public final double lat; - public final double lng; - public final double z; - - public LatLngZ (double latitude, double longitude) { - this(latitude, longitude, 0); - } - - public LatLngZ (double latitude, double longitude, double thirdDimension) { - this.lat = latitude; - this.lng = longitude; - this.z = thirdDimension; - } - - @Override - public String toString() { - return "LatLngZ [lat=" + lat + ", lng=" + lng + ", z=" + z + "]"; - } - - @Override - public boolean equals(Object anObject) { - if (this == anObject) { - return true; - } - if (anObject instanceof LatLngZ) { - LatLngZ passed = (LatLngZ)anObject; - if(passed.lat == this.lat && passed.lng == this.lng && passed.z == this.z) { - return true; - } - } - return false; - } - } -} diff --git a/java/src/com/here/flexpolyline/PolylineEncoderDecoderTest.java b/java/src/com/here/flexpolyline/PolylineEncoderDecoderTest.java deleted file mode 100644 index 6d19a44..0000000 --- a/java/src/com/here/flexpolyline/PolylineEncoderDecoderTest.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright (C) 2019 HERE Europe B.V. - * Licensed under MIT, see full license in LICENSE - * SPDX-License-Identifier: MIT - * License-Filename: LICENSE - */ -package com.here.flexpolyline; - -import static com.here.flexpolyline.PolylineEncoderDecoder.decode; -import static com.here.flexpolyline.PolylineEncoderDecoder.encode; -import static com.here.flexpolyline.PolylineEncoderDecoder.getThirdDimension; -import static com.here.flexpolyline.PolylineEncoderDecoder.ThirdDimension.ABSENT; -import static com.here.flexpolyline.PolylineEncoderDecoder.ThirdDimension.ALTITUDE; -import static com.here.flexpolyline.PolylineEncoderDecoder.ThirdDimension.ELEVATION; -import static com.here.flexpolyline.PolylineEncoderDecoder.ThirdDimension.LEVEL; - -import java.io.BufferedReader; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import com.here.flexpolyline.PolylineEncoderDecoder.Converter; -import com.here.flexpolyline.PolylineEncoderDecoder.LatLngZ; -import com.here.flexpolyline.PolylineEncoderDecoder.ThirdDimension; - -/** - * Validate polyline encoding with different input combinations. - */ -public class PolylineEncoderDecoderTest { - - private void testInvalidCoordinates() { - - //Null coordinates - assertThrows(IllegalArgumentException.class, - () -> { encode(null, 5, ThirdDimension.ABSENT, 0); }); - - - //Empty coordinates list test - assertThrows(IllegalArgumentException.class, - () -> { encode(new ArrayList(), 5, ThirdDimension.ABSENT, 0); }); - } - - private void testInvalidThirdDimension() { - - List pairs = new ArrayList<>(); - pairs.add(new LatLngZ(50.1022829, 8.6982122)); - ThirdDimension invalid = null; - - //Invalid Third Dimension - assertThrows(IllegalArgumentException.class, - () -> { encode(pairs, 5, invalid, 0); }); - } - - private void testConvertValue() { - - PolylineEncoderDecoder.Converter conv = new PolylineEncoderDecoder.Converter(5); - StringBuilder result = new StringBuilder(); - conv.encodeValue(-179.98321, result); - assertEquals(result.toString(), "h_wqiB"); - } - - private void testSimpleLatLngEncoding() { - - List pairs = new ArrayList<>(); - pairs.add(new LatLngZ(50.1022829, 8.6982122)); - pairs.add(new LatLngZ(50.1020076, 8.6956695)); - pairs.add(new LatLngZ(50.1006313, 8.6914960)); - pairs.add(new LatLngZ(50.0987800, 8.6875156)); - - String expected = "BFoz5xJ67i1B1B7PzIhaxL7Y"; - String computed = encode(pairs, 5, ThirdDimension.ABSENT, 0); - assertEquals(computed, expected); - } - - private void testComplexLatLngEncoding() { - - List pairs = new ArrayList<>(); - pairs.add(new LatLngZ(52.5199356, 13.3866272)); - pairs.add(new LatLngZ(52.5100899, 13.2816896)); - pairs.add(new LatLngZ(52.4351807, 13.1935196)); - pairs.add(new LatLngZ(52.4107285, 13.1964502)); - pairs.add(new LatLngZ(52.38871, 13.1557798)); - pairs.add(new LatLngZ(52.3727798, 13.1491003)); - pairs.add(new LatLngZ(52.3737488, 13.1154604)); - pairs.add(new LatLngZ(52.3875198, 13.0872202)); - pairs.add(new LatLngZ(52.4029388, 13.0706196)); - pairs.add(new LatLngZ(52.4105797, 13.0755529)); - - String expected = "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e"; - String computed = encode(pairs, 5, ThirdDimension.ABSENT, 0); - assertEquals(computed, expected); - } - - private void testLatLngZEncode() { - - List tuples = new ArrayList<>(); - tuples.add(new LatLngZ(50.1022829, 8.6982122, 10)); - tuples.add(new LatLngZ(50.1020076, 8.6956695, 20)); - tuples.add(new LatLngZ(50.1006313, 8.6914960, 30)); - tuples.add(new LatLngZ(50.0987800, 8.6875156, 40)); - - String expected = "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU"; - String computed = encode(tuples, 5, ThirdDimension.ALTITUDE, 0); - assertEquals(computed, expected); - } - - - /**********************************************/ - /********** Decoder test starts ***************/ - /**********************************************/ - private void testInvalidEncoderInput() { - - //Null coordinates - assertThrows(IllegalArgumentException.class, - () -> { decode(null); }); - - - //Empty coordinates list test - assertThrows(IllegalArgumentException.class, - () -> { decode(""); }); - } - - private void testThirdDimension() { - assertTrue(getThirdDimension("BFoz5xJ67i1BU") == ABSENT); - assertTrue(getThirdDimension("BVoz5xJ67i1BU") == LEVEL); - assertTrue(getThirdDimension("BlBoz5xJ67i1BU") == ALTITUDE); - assertTrue(getThirdDimension("B1Boz5xJ67i1BU") == ELEVATION); - } - - - private void testDecodeConvertValue() { - - String encoded = "h_wqiB"; - double expected = -179.98321; - AtomicReference computed = new AtomicReference<>(0d); - Converter conv = new Converter(5); - conv.decodeValue(encoded, - new AtomicInteger(0), - computed); - assertEquals(computed.get(), expected); - } - - - private void testSimpleLatLngDecoding() { - - List computed = decode("BFoz5xJ67i1B1B7PzIhaxL7Y"); - List expected = new ArrayList<>(); - expected.add(new LatLngZ(50.10228, 8.69821)); - expected.add(new LatLngZ(50.10201, 8.69567)); - expected.add(new LatLngZ(50.10063, 8.69150)); - expected.add(new LatLngZ(50.09878, 8.68752)); - - assertEquals(computed.size(), expected.size()); - for (int i = 0; i < computed.size(); ++i) { - assertEquals(computed.get(i), expected.get(i)); - } - } - - private void testComplexLatLngDecoding() { - - List computed = decode("BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e"); - - List pairs = new ArrayList<>(); - pairs.add(new LatLngZ(52.51994, 13.38663)); - pairs.add(new LatLngZ(52.51009, 13.28169)); - pairs.add(new LatLngZ(52.43518, 13.19352)); - pairs.add(new LatLngZ(52.41073, 13.19645)); - pairs.add(new LatLngZ(52.38871, 13.15578)); - pairs.add(new LatLngZ(52.37278, 13.14910)); - pairs.add(new LatLngZ(52.37375, 13.11546)); - pairs.add(new LatLngZ(52.38752, 13.08722)); - pairs.add(new LatLngZ(52.40294, 13.07062)); - pairs.add(new LatLngZ(52.41058, 13.07555)); - - assertEquals(computed.size(), pairs.size()); - for (int i = 0; i < computed.size(); ++i) { - assertEquals(computed.get(i), pairs.get(i)); - } - } - - private void testLatLngZDecode() { - List computed = decode("BlBoz5xJ67i1BU1B7PUzIhaUxL7YU"); - List tuples = new ArrayList<>(); - - tuples.add(new LatLngZ(50.10228, 8.69821, 10)); - tuples.add(new LatLngZ(50.10201, 8.69567, 20)); - tuples.add(new LatLngZ(50.10063, 8.69150, 30)); - tuples.add(new LatLngZ(50.09878, 8.68752, 40)); - - assertEquals(computed.size(), tuples.size()); - for (int i = 0; i < computed.size(); ++i) { - assertEquals(computed.get(i), tuples.get(i)); - } - } - - private static final String TEST_FILES_RELATIVE_PATH = "../test/"; - - private void encodingSmokeTest() { - - int lineNo = 0; - try (BufferedReader input = Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "original.txt")); - BufferedReader encoded = Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/encoded.txt"))) { - - String inputFileLine; - String encodedFileLine; - - // read line by line and validate the test - while ((inputFileLine = input.readLine()) != null && (encodedFileLine = encoded.readLine()) != null) { - - lineNo++; - int precision = 0; - int thirdDimPrecision = 0; - boolean hasThirdDimension = false; - ThirdDimension thirdDimension = ThirdDimension.ABSENT; - inputFileLine = inputFileLine.trim(); - encodedFileLine = encodedFileLine.trim(); - - //File parsing - String[] inputs = inputFileLine.substring(1, inputFileLine.length() - 1).split(";"); - String[] meta = inputs[0].trim().substring(1, inputs[0].trim().length() - 1).split(","); - precision = Integer.valueOf(meta[0]); - - if (meta.length > 1) { - thirdDimPrecision = Integer.valueOf(meta[1].trim()); - thirdDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim())); - hasThirdDimension = true; - } - List latLngZs = extractLatLngZ(inputs[1], hasThirdDimension); - String encodedComputed = encode(latLngZs, precision, thirdDimension, thirdDimPrecision); - String encodedExpected = encodedFileLine; - assertEquals(encodedComputed, encodedExpected); - } - } catch (Exception e) { - e.printStackTrace(); - System.err.format("LineNo: " + lineNo + " validation got exception: %s%n", e); - } - } - - private void decodingSmokeTest() { - - int lineNo = 0; - try (BufferedReader encoded = Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/encoded.txt")); - BufferedReader decoded = Files.newBufferedReader(Paths.get(TEST_FILES_RELATIVE_PATH + "round_half_up/decoded.txt"))) { - - String encodedFileLine; - String decodedFileLine; - - // read line by line and validate the test - while ((encodedFileLine = encoded.readLine()) != null && (decodedFileLine = decoded.readLine()) != null) { - - lineNo++; - boolean hasThirdDimension = false; - ThirdDimension expectedDimension = ThirdDimension.ABSENT; - encodedFileLine = encodedFileLine.trim(); - decodedFileLine = decodedFileLine.trim(); - - //File parsing - String[] output = decodedFileLine.substring(1, decodedFileLine.length() - 1).split(";"); - String[] meta = output[0].trim().substring(1, output[0].trim().length() - 1).split(","); - if (meta.length > 1) { - expectedDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim())); - hasThirdDimension = true; - } - String decodedInputLine = decodedFileLine.substring(1, decodedFileLine.length() - 1).split(";")[1]; - List expectedLatLngZs = extractLatLngZ(decodedInputLine, hasThirdDimension); - - //Validate thirdDimension - ThirdDimension computedDimension = getThirdDimension(encodedFileLine); - assertEquals(computedDimension, expectedDimension); - - //Validate LatLngZ - List computedLatLngZs = decode(encodedFileLine); - assertEquals(computedLatLngZs.size(), expectedLatLngZs.size()); - for (int i = 0; i < computedLatLngZs.size(); ++i) { - assertEquals(computedLatLngZs.get(i), expectedLatLngZs.get(i)); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.err.format("LineNo: " + lineNo + " validation got exception: %s%n", e); - } - } - - private static List extractLatLngZ(String line, boolean hasThirdDimension) { - List latLngZs = new ArrayList(); - - String[] coordinates = line.trim().substring(1, line.trim().length()-1).split(","); - for(int itr = 0; itr < coordinates.length && !isNullOrEmpty(coordinates[itr]); ) { - double lat = Double.valueOf(coordinates[itr++].trim().replace("(", "")); - double lng = Double.valueOf(coordinates[itr++].trim().replace(")", "")); - double z = 0; - if(hasThirdDimension) { - z = Double.valueOf(coordinates[itr++].trim().replace(")", "")); - } - latLngZs.add(new LatLngZ(lat, lng, z)); - } - return latLngZs; - } - - public static boolean isNullOrEmpty(String str) { - if(str != null && !str.trim().isEmpty()) { - return false; - } - return true; - } - - private static void assertEquals(Object lhs, Object rhs) { - if (lhs != rhs) { - if (!lhs.equals(rhs)) { - throw new RuntimeException("Assert failed, " + lhs + " != " + rhs); - } - } - } - - private static void assertTrue(boolean value) { - if (!value) { - throw new RuntimeException("Assert failed"); - } - } - - private static void assertThrows(Class expectedType, Runnable runnable) { - try { - runnable.run(); - } - catch (Throwable actualException) { - if (!expectedType.isInstance(actualException)) { - throw new RuntimeException("Assert failed, Invalid exception found!"); - } - return; - } - throw new RuntimeException("Assert failed, No exception found!"); - } - - public static void main(String[] args) { - PolylineEncoderDecoderTest test = new PolylineEncoderDecoderTest(); - test.testInvalidCoordinates(); - test.testInvalidThirdDimension(); - test.testConvertValue(); - test.testSimpleLatLngEncoding(); - test.testComplexLatLngEncoding(); - test.testLatLngZEncode(); - test.encodingSmokeTest(); - - //Decode test - test.testInvalidEncoderInput(); - test.testThirdDimension(); - test.testDecodeConvertValue(); - test.testSimpleLatLngDecoding(); - test.testComplexLatLngDecoding(); - test.testLatLngZDecode(); - test.decodingSmokeTest(); - } -} diff --git a/javascript/.gitignore b/javascript/.gitignore deleted file mode 100644 index c2658d7..0000000 --- a/javascript/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/javascript/index.js b/javascript/index.js deleted file mode 100644 index d0fcca9..0000000 --- a/javascript/index.js +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (C) 2019 HERE Europe B.V. - * Licensed under MIT, see full license in LICENSE - * SPDX-License-Identifier: MIT - * License-Filename: LICENSE - */ -const DEFAULT_PRECISION = 5; - -const ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - -const DECODING_TABLE = [ - 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 -]; - -const FORMAT_VERSION = 1; - -const ABSENT = 0; -const LEVEL = 1; -const ALTITUDE = 2; -const ELEVATION = 3; -// Reserved values 4 and 5 should not be selectable -const CUSTOM1 = 6; -const CUSTOM2 = 7; - -const Num = typeof BigInt !== "undefined" ? BigInt : Number; - -function decode(encoded) { - const decoder = decodeUnsignedValues(encoded); - const header = decodeHeader(decoder[0], decoder[1]); - - const factorDegree = 10 ** header.precision; - const factorZ = 10 ** header.thirdDimPrecision; - const { thirdDim } = header; - - let lastLat = 0; - let lastLng = 0; - let lastZ = 0; - const res = []; - - let i = 2; - for (;i < decoder.length;) { - const deltaLat = toSigned(decoder[i]) / factorDegree; - const deltaLng = toSigned(decoder[i + 1]) / factorDegree; - lastLat += deltaLat; - lastLng += deltaLng; - - if (thirdDim) { - const deltaZ = toSigned(decoder[i + 2]) / factorZ; - lastZ += deltaZ; - res.push([lastLat, lastLng, lastZ]); - i += 3; - } else { - res.push([lastLat, lastLng]); - i += 2; - } - } - - if (i !== decoder.length) { - throw new Error('Invalid encoding. Premature ending reached'); - } - - return { - ...header, - polyline: res, - }; -} - -function decodeChar(char) { - const charCode = char.charCodeAt(0); - return DECODING_TABLE[charCode - 45]; -} - -function decodeUnsignedValues(encoded) { - let result = Num(0); - let shift = Num(0); - const resList = []; - - encoded.split('').forEach((char) => { - const value = Num(decodeChar(char)); - result |= (value & Num(0x1F)) << shift; - if ((value & Num(0x20)) === Num(0)) { - resList.push(result); - result = Num(0); - shift = Num(0); - } else { - shift += Num(5); - } - }); - - if (shift > 0) { - throw new Error('Invalid encoding'); - } - - return resList; -} - -function decodeHeader(version, encodedHeader) { - if (+version.toString() !== FORMAT_VERSION) { - throw new Error('Invalid format version'); - } - const headerNumber = +encodedHeader.toString(); - const precision = headerNumber & 15; - const thirdDim = (headerNumber >> 4) & 7; - const thirdDimPrecision = (headerNumber >> 7) & 15; - return { precision, thirdDim, thirdDimPrecision }; -} - -function toSigned(val) { - // Decode the sign from an unsigned value - let res = val; - if (res & Num(1)) { - res = ~res; - } - res >>= Num(1); - return +res.toString(); -} - -function encode({ precision = DEFAULT_PRECISION, thirdDim = ABSENT, thirdDimPrecision = 0, polyline }) { - // Encode a sequence of lat,lng or lat,lng(,{third_dim}). Note that values should be of type BigNumber - // `precision`: how many decimal digits of precision to store the latitude and longitude. - // `third_dim`: type of the third dimension if present in the input. - // `third_dim_precision`: how many decimal digits of precision to store the third dimension. - - const multiplierDegree = 10 ** precision; - const multiplierZ = 10 ** thirdDimPrecision; - const encodedHeaderList = encodeHeader(precision, thirdDim, thirdDimPrecision); - const encodedCoords = []; - - let lastLat = Num(0); - let lastLng = Num(0); - let lastZ = Num(0); - polyline.forEach((location) => { - const lat = Num(Math.round(location[0] * multiplierDegree)); - encodedCoords.push(encodeScaledValue(lat - lastLat)); - lastLat = lat; - - const lng = Num(Math.round(location[1] * multiplierDegree)); - encodedCoords.push(encodeScaledValue(lng - lastLng)); - lastLng = lng; - - if (thirdDim) { - const z = Num(Math.round(location[2] * multiplierZ)); - encodedCoords.push(encodeScaledValue(z - lastZ)); - lastZ = z; - } - }); - - return [...encodedHeaderList, ...encodedCoords].join(''); -} - -function encodeHeader(precision, thirdDim, thirdDimPrecision) { - // Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char - if (precision < 0 || precision > 15) { - throw new Error('precision out of range. Should be between 0 and 15'); - } - if (thirdDimPrecision < 0 || thirdDimPrecision > 15) { - throw new Error('thirdDimPrecision out of range. Should be between 0 and 15'); - } - if (thirdDim < 0 || thirdDim > 7 || thirdDim === 4 || thirdDim === 5) { - throw new Error('thirdDim should be between 0, 1, 2, 3, 6 or 7'); - } - - const res = (thirdDimPrecision << 7) | (thirdDim << 4) | precision; - return encodeUnsignedNumber(FORMAT_VERSION) + encodeUnsignedNumber(res); -} - -function encodeUnsignedNumber(val) { - // Uses variable integer encoding to encode an unsigned integer. Returns the encoded string. - let res = ''; - let numVal = Num(val); - while (numVal > 0x1F) { - const pos = (numVal & Num(0x1F)) | Num(0x20); - res += ENCODING_TABLE[pos]; - numVal >>= Num(5); - } - return res + ENCODING_TABLE[numVal]; -} - -function encodeScaledValue(value) { - // Transform a integer `value` into a variable length sequence of characters. - // `appender` is a callable where the produced chars will land to - let numVal = Num(value); - const negative = numVal < 0; - numVal <<= Num(1); - if (negative) { - numVal = ~numVal; - } - - return encodeUnsignedNumber(numVal); -} - -module.exports = { - encode, - decode, - - ABSENT, - LEVEL, - ALTITUDE, - ELEVATION, -}; diff --git a/javascript/package-lock.json b/javascript/package-lock.json deleted file mode 100644 index 5062163..0000000 --- a/javascript/package-lock.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "@here/flexpolyline", - "version": "0.1.0", - "lockfileVersion": 1 -} diff --git a/javascript/package.json b/javascript/package.json deleted file mode 100644 index f536eb8..0000000 --- a/javascript/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@here/flexpolyline", - "version": "0.1.0", - "description": "Flexible Polyline encoding: a lossy compressed representation of a list of coordinate pairs or triples", - "main": "index.js", - "scripts": { - "test": "node test/test.js" - }, - "keywords": [ - "polyline", - "encoding" - ], - "author": { - "name": "HERE Europe B.V.", - "url": "https://here.com" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/heremaps/flexible-polyline.git", - "directory": "javascript" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/javascript/test/test.js b/javascript/test/test.js deleted file mode 100644 index 36131e9..0000000 --- a/javascript/test/test.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2019 HERE Europe B.V. - * Licensed under MIT, see full license in LICENSE - * SPDX-License-Identifier: MIT - * License-Filename: LICENSE - */ -const poly = require("../"); -const assert = require('assert'); -const fs = require('fs'); - -const originalLines = fs.readFileSync('../test/original.txt', { encoding: 'utf-8' }).split('\n'); -const encodedLines = fs.readFileSync('../test/encoded.txt', { encoding: 'utf-8' }).split('\n'); -const decodedLines = fs.readFileSync('../test/decoded.txt', { encoding: 'utf-8' }).split('\n'); - -function runTests() { - originalLines.forEach((original, index) => { - if (!original) { - return; - } - const input = parseLine(original); - if (input.thirdDim === 4 || input.thirdDim === 5 || input.precision > 10 || input.thirdDimPrecision > 10) { - return; - } - const encoded = encodedLines[index]; - const decoded = decodedLines[index].replace(/ /g, ''); - - const encodedInput = poly.encode(input); - assert.strictEqual(encodedInput, encoded); - - const expectedDecoded = parseLine(decoded); - const resDecoded = poly.decode(encodedInput); - assert.strictEqual(resDecoded.precision, expectedDecoded.precision); - assert.strictEqual(resDecoded.thirdDim, expectedDecoded.thirdDim || 0); - assert.strictEqual(resDecoded.thirdDimPrecision, expectedDecoded.thirdDimPrecision); - expectedDecoded.polyline.forEach((expectedPos, i0) => { - expectedPos.forEach((val, i1) => { - const precision = i1 === 2 ? resDecoded.thirdDimPrecision : resDecoded.precision; - assert(approxEq(val, resDecoded.polyline[i0][i1], 1 / (10 ** precision))); - }); - }); - }); -} - -runTests(); - -function parseLine(line) { - // Strip off all spaces, curly braces, square brackets and trailing comma - const [rawHeader, rawPolyline] = line.replace(/[ {}\[\]]/g, '').slice(0, -1).split(';'); - const [precision, thirdDimPrecision, thirdDim] = rawHeader.slice(1, -1).split(',').map((num) => num ? +num : undefined); - const polyline = rawPolyline.split('),(').map((rawLocation) => { - return rawLocation.replace(/[()]/g, '').split(',').map((num) => +num); - }); - - return { precision, thirdDim, thirdDimPrecision, polyline }; -} - -function approxEq(v1, v2, epsilon) { - if (epsilon == null) { - epsilon = 0.001; - } - return Math.abs(v1 - v2) < epsilon; -} - -console.log('Tests succeeded'); diff --git a/php/.gitattributes b/php/.gitattributes deleted file mode 100644 index 0886945..0000000 --- a/php/.gitattributes +++ /dev/null @@ -1,16 +0,0 @@ -# Path-based git attributes -# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html - -# Ignore all test and documentation with "export-ignore". -/.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.scrutinizer.yml export-ignore -/.styleci.yml export-ignore -/.travis.yml export-ignore -/PULL_REQUEST_TEMPLATE.md export-ignore -/ISSUE_TEMPLATE.md export-ignore -/phpcs.xml.dist export-ignore -/phpunit.xml.dist export-ignore -/tests export-ignore -/docs export-ignore \ No newline at end of file diff --git a/php/.gitignore b/php/.gitignore deleted file mode 100644 index 75e12b3..0000000 --- a/php/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -build -composer.lock -vendor -phpcs.xml -phpunit.xml -.phpunit.result.cache \ No newline at end of file diff --git a/php/.travis.yml b/php/.travis.yml deleted file mode 100644 index 087636f..0000000 --- a/php/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -dist: trusty -language: php - -php: - - 7.2 - - 7.3 - - 7.4 - -## Cache composer -cache: - directories: - - $HOME/.composer/cache - -matrix: - include: - - php: 7.2 - env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' - -before_script: - - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist - -script: - - vendor/bin/phpcs --standard=psr2 src/ - -after_script: - - | - if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' ]]; then - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.clover - fi \ No newline at end of file diff --git a/php/README.md b/php/README.md deleted file mode 100644 index 98a7978..0000000 --- a/php/README.md +++ /dev/null @@ -1,55 +0,0 @@ -## Testing - -``` -composer test -``` - -## Usage - -### Decode - -FlexiblePolyline::decode(string $encoded): array - -``` -$data = FlexiblePolyline::decode('BlBoz5xJ67i1BU1B7PUzIhaUxL7YU'); -/** $data: -[ - 'precision' => 5, - 'thirdDim' => 2, - 'thirdDimPrecision' => 0, - 'polyline' => [ - [50.10228, 8.69821, 10], - [50.10201, 8.69567, 20], - [50.10063, 8.6915, 30], - [50.09878, 8.68752, 40] - ] -] -*/ -``` - -### Encode - -FlexiblePolyline::encode(array $coordinates [, int $precision = null, int $thirdDim = null, int $thirdDimPrecision = 0]): string - -``` -$encoded = FlexiblePolyline::encode([ - [50.10228, 8.69821, 10], - [50.10201, 8.69567, 20], - [50.10063, 8.6915, 30], - [50.09878, 8.68752, 40] -], 5, 2, 0); -/** $encoded: -BlBoz5xJ67i1BU1B7PUzIhaUxL7YU -*/ -``` - -### Third Dimension - -FlexiblePolyline::getThirdDimension(string $encoded): int - -``` -$thirdDimension = FlexiblePolyline::getThirdDimension('BVoz5xJ67i1BU') -/** $thirdDimension: -1 -*/ -``` \ No newline at end of file diff --git a/php/composer.json b/php/composer.json deleted file mode 100644 index 72f925d..0000000 --- a/php/composer.json +++ /dev/null @@ -1,44 +0,0 @@ - -{ - "name": "heremaps/flexible-polyline", - "type": "library", - "description": "Flexible Polyline encoding: a lossy compressed representation of a list of coordinate pairs or triples", - "keywords": [ - "heremaps", - "flexible-polyline" - ], - "homepage": "https://github.com/heremaps/flexible-polyline", - "license": "MIT", - "authors": [ - { - "name": "rozklad", - "email": "jan.rozklad@gmail.com" - } - ], - "require": { - "php" : "~7.2", - "ext-mbstring": "*" - }, - "require-dev": { - "phpunit/phpunit" : ">=8.0", - "squizlabs/php_codesniffer": "^3.0" - }, - "autoload": { - "psr-4": { - "Heremaps\\FlexiblePolyline\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Heremaps\\FlexiblePolyline\\Tests\\": "tests" - } - }, - "scripts": { - "test": "phpunit tests", - "check-style": "phpcs src tests", - "fix-style": "phpcbf src tests" - }, - "config": { - "sort-packages": true - } -} diff --git a/php/src/FlexiblePolyline.php b/php/src/FlexiblePolyline.php deleted file mode 100644 index 11a1e1e..0000000 --- a/php/src/FlexiblePolyline.php +++ /dev/null @@ -1,31 +0,0 @@ - $header['precision'], - 'thirdDim' => $header['thirdDim'], - 'thirdDimPrecision' => $header['thirdDimPrecision'], - 'polyline' => $res - ]; - } - - public static function decodeHeader(int $version, int $encodedHeader): array - { - if ($version !== self::FORMAT_VERSION) { - throw new Exception('Invalid format version'); - } - $headerNumber = (string)+$encodedHeader; - $precision = $headerNumber & 15; - $thirdDim = ($headerNumber >> 4) & 7; - $thirdDimPrecision = ($headerNumber >> 7) & 15; - return compact('precision', 'thirdDim', 'thirdDimPrecision'); - } - - public static function toSigned(int $val): string - { - $res = $val; - if ($res & 1) { - $res = ~$res; - } - $res >>= 1; - return (string)+$res; - } - - public static function decodeUnsignedValues(string $encoded): array - { - $result = 0; - $shift = 0; - $resList = []; - - $characters = str_split($encoded); - - foreach ($characters as $char) { - $value = self::decodeChar($char); - $result |= ($value & 0x1F) << $shift; - - if (($value & 0x20) === 0) { - $resList[] = $result; - $result = 0; - $shift = 0; - } else { - $shift += 5; - } - } - - if ($shift > 0) { - throw new Exception('Invalid encoding'); - } - - return $resList; - } - - public static function decodeChar(string $char): string - { - try { - $charcode = mb_ord($char); - $decoded = self::DECODING_TABLE[$charcode - 45]; - } catch (Exception $e) { - throw new Exception('Char could not be decoded'); - } - - return $decoded; - } - - public static function getThirdDimension(string $encoded): int - { - $decoded = self::decodeUnsignedValues($encoded); - $header = self::decodeHeader($decoded[0], $decoded[1]); - return $header['thirdDim']; - } -} diff --git a/php/src/Traits/EncodableTrait.php b/php/src/Traits/EncodableTrait.php deleted file mode 100644 index 5713986..0000000 --- a/php/src/Traits/EncodableTrait.php +++ /dev/null @@ -1,94 +0,0 @@ - 15) { - throw new Exception('precision out of range. Should be between 0 and 15'); - } - if ($thirdDimPrecision < 0 || $thirdDimPrecision > 15) { - throw new Exception('thirdDimPrecision out of range. Should be between 0 and 15'); - } - if ($thirdDim < 0 || $thirdDim > 7 || $thirdDim === 4 || $thirdDim === 5) { - throw new Exception('thirdDim should be between 0, 1, 2, 3, 6 or 7'); - } - - $res = ($thirdDimPrecision << 7) | ($thirdDim << 4) | $precision; - return self::encodeUnsignedNumber(self::FORMAT_VERSION) . self::encodeUnsignedNumber($res); - } - - public static function encodeUnsignedNumber(float $val): string - { - $res = ''; - $numVal = (float)$val; - while ($numVal > 0x1F) { - $pos = ($numVal & 0x1F) | 0x20; - $pos = (int)$pos; - $res .= self::ENCODING_TABLE[$pos]; - $numVal >>= 5; - } - $numVal = (int)$numVal; - return $res . self::ENCODING_TABLE[$numVal]; - } - - public static function encodeScaledValue(float $value): string - { - $negative = $value < 0; - $value <<= 1; - if ($negative) { - $value = ~$value; - } - - return self::encodeUnsignedNumber($value); - } -} diff --git a/php/tests/DecoderTest.php b/php/tests/DecoderTest.php deleted file mode 100644 index 062cff5..0000000 --- a/php/tests/DecoderTest.php +++ /dev/null @@ -1,49 +0,0 @@ -runFolder($folder); - } - } - - public function runFolder(string $folder): void - { - $originalLines = self::getOriginalLines(); - $encodedLines = self::getEncodedLines($folder); - $decodedLines = self::getDecodedLines($folder); - - $results = []; - - for($i = 0; $i < count($encodedLines); $i++) - { - $input = self::parseLine($originalLines[$i]); - $encoded = $encodedLines[$i]; - $decoded = $decodedLines[$i]; - - if ($input['thirdDim'] === 4 || $input['thirdDim'] === 5 || $input['thirdDimPrecision'] > 10 || $input['precision'] > 10) { - // Test decoding only - $expectedDecoded = self::parseLine($decoded); - $decodedEncodedValue = FlexiblePolyline::decode($encoded); - $this->assertEqualsCanonicalizing($expectedDecoded, $decodedEncodedValue); - } else { - // Test full - $expectedDecoded = self::parseLine($decoded); - $encodedInput = FlexiblePolyline::encode($input['polyline'], $input['precision'], $input['thirdDim'], $input['thirdDimPrecision']); - $decodedInput = FlexiblePolyline::decode($encodedInput); - - $this->assertEqualsCanonicalizing($expectedDecoded, $decodedInput); - } - } - } - -} \ No newline at end of file diff --git a/php/tests/EncoderTest.php b/php/tests/EncoderTest.php deleted file mode 100644 index 4e8305f..0000000 --- a/php/tests/EncoderTest.php +++ /dev/null @@ -1,40 +0,0 @@ -runFolder($folder); - } - } - - public function runFolder(string $folder): void - { - $originalLines = self::getOriginalLines(); - $encodedLines = self::getEncodedLines($folder); - - $results = []; - - for($i = 0; $i < count($encodedLines); $i++) { - $input = self::parseLine($originalLines[$i]); - $encodedInput = $encodedLines[$i]; - - if ($input['thirdDim'] === 4 || $input['thirdDim'] === 5 || $input['thirdDimPrecision'] > 10 || $input['precision'] > 10) { - continue; - } - - $encodedResult = FlexiblePolyline::encode($input['polyline'], $input['precision'], $input['thirdDim'], $input['thirdDimPrecision']); - - $this->assertEquals($encodedInput, $encodedResult); - } - } - -} \ No newline at end of file diff --git a/php/tests/FlexiblePolylineTest.php b/php/tests/FlexiblePolylineTest.php deleted file mode 100644 index e04c97c..0000000 --- a/php/tests/FlexiblePolylineTest.php +++ /dev/null @@ -1,66 +0,0 @@ - 5, - 'thirdDim' => 2, - 'thirdDimPrecision' => 0, - 'polyline' => [ - [50.10228, 8.69821, 10], - [50.10201, 8.69567, 20], - [50.10063, 8.6915, 30], - [50.09878, 8.68752, 40] - ] - ]; - - $this->assertEqualsCanonicalizing($expected, $decoded); - } - - public function testReadmeExampleEncode(): void - { - $encoded = FlexiblePolyline::encode( - [ - [50.10228, 8.69821, 10], - [50.10201, 8.69567, 20], - [50.10063, 8.6915, 30], - [50.09878, 8.68752, 40] - ], 5, 2, 0 - ); - - $expected = 'BlBoz5xJ67i1BU1B7PUzIhaUxL7YU'; - - $this->assertEquals($expected, $encoded); - } - - public function testThirdDimension(): void - { - $this->assertEquals(FlexiblePolyline::getThirdDimension('BFoz5xJ67i1BU'), FlexiblePolyline::ABSENT); - $this->assertEquals(FlexiblePolyline::getThirdDimension('BVoz5xJ67i1BU'), FlexiblePolyline::LEVEL); - $this->assertEquals(FlexiblePolyline::getThirdDimension('BlBoz5xJ67i1BU'), FlexiblePolyline::ALTITUDE); - $this->assertEquals(FlexiblePolyline::getThirdDimension('B1Boz5xJ67i1BU'), FlexiblePolyline::ELEVATION); - } - -} \ No newline at end of file diff --git a/python/.gitignore b/python/.gitignore deleted file mode 100644 index 438227b..0000000 --- a/python/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -_env/ -__pycache__/ -*.pyc -build/ -dist/ -*.egg-info/ -.eggs/ -_env* -LICENSE diff --git a/python/MANIFEST.in b/python/MANIFEST.in deleted file mode 100644 index 689e50f..0000000 --- a/python/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include LICENSE -include README.md \ No newline at end of file diff --git a/python/README.md b/python/README.md deleted file mode 100644 index ea4ca0c..0000000 --- a/python/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# FlexPolyline - -This is a python implementation of the Flexible Polyline format. - -The polyline encoding is a lossy compressed representation of a list of coordinate pairs or -coordinate triples. It achieves that by: - -1. Reducing the decimal digits of each value. -2. Encoding only the offset from the previous point. -3. Using variable length for each coordinate delta. -4. Using 64 URL-safe characters to display the result. - -## Install - -```python -pip install flexpolyline -``` - -## Usage - -### Encoding - -#### `encode(iterable, precision=5, third_dim=ABSENT, third_dim_precision=0)` - -Encodes a list (or iterator) of coordinates to the corresponding string representation. See the optional parameters below for further customization. Coordinate order is `(lat, lng[, third_dim])`. -``` - -**Optional parameters** - -* `precision` - Defines how many decimal digits to round latitude and longitude to (ranges from `0` to `15`). -* `third_dim` - Defines the type of the third dimension when present. Possible values are defined in the module: `ALTITUDE`, `LEVEL`, `ELEVATION`, `CUSTOM1` and `CUSTOM2`. The last two values can be used in case your third dimension has a user defined meaning. -* `third_dim_precision` - Defines how many decimal digits to round the third dimension to (ranges from `0` to `15`). This parameter is ignored when `third_dim` is `ABSENT` (default). - - -#### `dict_encode(iterable, precision=5, third_dim=ABSENT, third_dim_precision=0)` - -Similar to the `encode` function, but accepts a list (or iterator) of dictionaries instead. Required keys are `"lat"` and `"lng"`. If `third_dim` is set, the corresponding key is expected `"alt"`, `"elv"`, `"lvl"`, `"cst1"` or `"cst2"`. - - -#### Examples - -Following is a simple example encoding a 2D poyline with 5 decimal digits of precision: - -```python -import flexpolyline as fp - -example = [ - (50.1022829, 8.6982122), - (50.1020076, 8.6956695), - (50.1006313, 8.6914960), - (50.0987800, 8.6875156), -] - -print(fp.encode(example)) -``` - -**Output**: `BFoz5xJ67i1B1B7PzIhaxL7Y`. - -Another example for the 3D case with altitude as the third coordinate: - -```python -example = [ - (50.1022829, 8.6982122, 10), - (50.1020076, 8.6956695, 20), - (50.1006313, 8.6914960, 30), - (50.0987800, 8.6875156, 40), -] - -print(fp.encode(example, third_dim=flexpolyline.ALTITUDE)) -``` - -**Output**: `BlBoz5xJ67i1BU1B7PUzIhaUxL7YU` - -### Decoding - -#### `decode(encoded_string)` - -Decodes the passed encoded string and returns a list of tuples `(lat, lng[, third_dim])`. - -#### `iter_decode(encoded_string)` - -Similar to `decode` but returns an iterator instead. - -#### `dict_decode(encoded_string)` - -Similar to `decode` but returns a list of dictionaries instead. The keys `"lat"` and `"lng"` are always present, while the third dimension key depends on the type of third dimension encoded. It can be one of the following: `"alt"`, `"elv"`, `"lvl"`, `"cst1"` or `"cst2"`. - -#### `iter_dict_decode(encoded_string)` - -Similar to `dict_decode` but returns an iterator instead. - -#### `get_third_dimension(encoded_string)` - -Returns the value corresponding to the third dimension encoded in the string. Possible values defined in the module are: `ABSENT`, `ALTITUDE`, `LEVEL`, `ELEVATION`, `CUSTOM1` and `CUSTOM2` - -#### Examples - -Example of decoding of a 2D polyline: - -```python -import flexpolyline as fp - -print(fp.decode("BFoz5xJ67i1B1B7PzIhaxL7Y")) -``` - -**Output**: - -``` -[ - (50.10228, 8.69821), - (50.10201, 8.69567), - (50.10063, 8.69150), - (50.09878, 8.68752) -] -``` - - -Example of decoding dicts from a 3D polyline: - -```python -import flexpolyline as fp - -print(fp.dict_decode("BlBoz5xJ67i1BU1B7PUzIhaUxL7YU")) -``` - -**Output**: - -``` -[ - {'lat': 50.10228, 'lng': 8.69821, 'alt': 10}, - {'lat': 50.10201, 'lng': 8.69567, 'alt': 20}, - {'lat': 50.10063, 'lng': 8.69150, 'alt': 30}, - {'lat': 50.09878, 'lng': 8.68752, 'alt': 40} -] -``` - - - - diff --git a/python/flexpolyline/__init__.py b/python/flexpolyline/__init__.py deleted file mode 100644 index b225afc..0000000 --- a/python/flexpolyline/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2019 HERE Europe B.V. -# Licensed under MIT, see full license in LICENSE -# SPDX-License-Identifier: MIT -# License-Filename: LICENSE - -from .encoding import _dict_to_tuple, ABSENT, ALTITUDE, LEVEL, ELEVATION, CUSTOM1, CUSTOM2 -from .decoding import THIRD_DIM_MAP, get_third_dimension - -from .decoding import iter_decode -from .encoding import encode - - -def dict_encode(coordinates, precision=5, third_dim=ABSENT, third_dim_precision=0): - """Encode the sequence of coordinates dicts into a polyline string""" - return encode( - _dict_to_tuple(coordinates, third_dim), - precision=precision, - third_dim=third_dim, - third_dim_precision=third_dim_precision - ) - - -def decode(encoded): - """Return a list of coordinates. The number of coordinates are 2 or 3 - depending on the polyline content.""" - return list(iter_decode(encoded)) - - -def iter_dict_decode(encoded): - """Return an iterator over coordinates dicts. The dict contains always the keys 'lat', 'lng' and - depending on the polyline can contain a third key ('elv', 'lvl', 'alt', ...).""" - third_dim_key = THIRD_DIM_MAP[get_third_dimension(encoded)] - for row in iter_decode(encoded): - yield { - 'lat': row[0], - 'lng': row[1], - third_dim_key: row[2] - } - - -def dict_decode(encoded): - """Return an list of coordinates dicts. The dict contains always the keys 'lat', 'lng' and - depending on the polyline can contain a third key ('elv', 'lvl' or 'alt').""" - return list(iter_dict_decode(encoded)) \ No newline at end of file diff --git a/python/flexpolyline/decoding.py b/python/flexpolyline/decoding.py deleted file mode 100644 index 2f89b56..0000000 --- a/python/flexpolyline/decoding.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (C) 2019 HERE Europe B.V. -# Licensed under MIT, see full license in LICENSE -# SPDX-License-Identifier: MIT -# License-Filename: LICENSE - -from collections import namedtuple - -from .encoding import THIRD_DIM_MAP, FORMAT_VERSION - -__all__ = [ - 'decode', 'dict_decode', 'iter_decode', - 'get_third_dimension', 'decode_header', 'PolylineHeader' -] - -DECODING_TABLE = [ - 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, - 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 -] - - -PolylineHeader = namedtuple('PolylineHeader', 'precision,third_dim,third_dim_precision') - - -def decode_header(decoder): - """Decode the polyline header from an `encoded_char`. Returns a PolylineHeader object.""" - version = next(decoder) - if version != FORMAT_VERSION: - raise ValueError('Invalid format version') - value = next(decoder) - precision = value & 15 - value >>= 4 - third_dim = value & 7 - third_dim_precision = (value >> 3) & 15 - return PolylineHeader(precision, third_dim, third_dim_precision) - - -def get_third_dimension(encoded): - """Return the third dimension of an encoded polyline. - Possible returned values are: ABSENT, LEVEL, ALTITUDE, ELEVATION, CUSTOM1, CUSTOM2.""" - header = decode_header(decode_unsigned_values(encoded)) - return header.third_dim - - -def decode_char(char): - """Decode a single char to the corresponding value""" - char_value = ord(char) - - try: - value = DECODING_TABLE[char_value - 45] - except IndexError: - raise ValueError('Invalid encoding') - if value < 0: - raise ValueError('Invalid encoding') - return value - - -def to_signed(value): - """Decode the sign from an unsigned value""" - if value & 1: - value = ~value - value >>= 1 - return value - - -def decode_unsigned_values(encoded): - """Return an iterator over encoded unsigned values part of an `encoded` polyline""" - result = shift = 0 - - for char in encoded: - value = decode_char(char) - - result |= (value & 0x1F) << shift - if (value & 0x20) == 0: - yield result - result = shift = 0 - else: - shift += 5 - - if shift > 0: - raise ValueError('Invalid encoding') - - -def iter_decode(encoded): - """Return an iterator over coordinates. The number of coordinates are 2 or 3 - depending on the polyline content.""" - - last_lat = last_lng = last_z = 0 - decoder = decode_unsigned_values(encoded) - - header = decode_header(decoder) - factor_degree = 10.0 ** header.precision - factor_z = 10.0 ** header.third_dim_precision - third_dim = header.third_dim - - while True: - try: - last_lat += to_signed(next(decoder)) - except StopIteration: - return # sequence completed - - try: - last_lng += to_signed(next(decoder)) - - if third_dim: - last_z += to_signed(next(decoder)) - yield (last_lat / factor_degree, last_lng / factor_degree, last_z / factor_z) - else: - yield (last_lat / factor_degree, last_lng / factor_degree) - except StopIteration: - raise ValueError("Invalid encoding. Premature ending reached") diff --git a/python/flexpolyline/encoding.py b/python/flexpolyline/encoding.py deleted file mode 100644 index b58e17f..0000000 --- a/python/flexpolyline/encoding.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (C) 2019 HERE Europe B.V. -# Licensed under MIT, see full license in LICENSE -# SPDX-License-Identifier: MIT -# License-Filename: LICENSE - -from collections import namedtuple -import warnings - -__all__ = ['ABSENT', 'LEVEL', 'ALTITUDE', 'ELEVATION', 'encode', 'dict_encode', 'THIRD_DIM_MAP'] - -ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" - -FORMAT_VERSION = 1 - -ABSENT = 0 -LEVEL = 1 -ALTITUDE = 2 -ELEVATION = 3 -# Reserved values 4 and 5 should not be selectable -CUSTOM1 = 6 -CUSTOM2 = 7 - -THIRD_DIM_MAP = {ALTITUDE: 'alt', ELEVATION: 'elv', LEVEL: 'lvl', CUSTOM1: 'cst1', CUSTOM2: 'cst2'} - -PolylineHeader = namedtuple('PolylineHeader', 'precision,third_dim,third_dim_precision') - - -def encode_unsigned_varint(value, appender): - """Uses veriable integer encoding to encode an unsigned integer. - Returns the encoded string.""" - while value > 0x1F: - pos = (value & 0x1F) | 0x20 - appender(ENCODING_TABLE[pos]) - value >>= 5 - appender(ENCODING_TABLE[value]) - - -def encode_scaled_value(value, appender): - """Transform a integer `value` into a variable length sequence of characters. - `appender` is a callable where the produced chars will land to""" - negative = value < 0 - - value = value << 1 - if negative: - value = ~value - - encode_unsigned_varint(value, appender) - - -def encode_header(appender, precision, third_dim, third_dim_precision): - """Encode the `precision`, `third_dim` and `third_dim_precision` into one - encoded char""" - if precision < 0 or precision > 15: - raise ValueError("precision out of range") - if third_dim_precision < 0 or third_dim_precision > 15: - raise ValueError("third_dim_precision out of range") - if third_dim < 0 or third_dim > 7: - raise ValueError("third_dim out of range") - if third_dim == 4 or third_dim == 5: - warnings.warn("Third dimension types 4 and 5 are reserved and should not be used " - "as meaning may change in the future") - - res = (third_dim_precision << 7) | (third_dim << 4) | precision - encode_unsigned_varint(FORMAT_VERSION, appender) - encode_unsigned_varint(res, appender) - - -def encode(coordinates, precision=5, third_dim=ABSENT, third_dim_precision=0): - """Encode a sequence of lat,lng or lat,lng(,{third_dim}). - `precision`: how many decimal digits of precision to store the latitude and longitude. - `third_dim`: type of the third dimension if present in the input. - `third_dim_precision`: how many decimal digits of precision to store the third dimension.""" - multiplier_degree = 10 ** precision - multiplier_z = 10 ** third_dim_precision - - last_lat = last_lng = last_z = 0 - - res = [] - appender = res.append - encode_header(appender, precision, third_dim, third_dim_precision) - - for location in coordinates: - lat = int(round(location[0] * multiplier_degree)) - encode_scaled_value(lat - last_lat, appender) - last_lat = lat - - lng = int(round(location[1] * multiplier_degree)) - encode_scaled_value(lng - last_lng, appender) - last_lng = lng - - if third_dim: - z = int(round(location[2] * multiplier_z)) - encode_scaled_value(z - last_z, appender) - last_z = z - - return ''.join(res) - - -def _dict_to_tuple(coordinates, third_dim): - """Convert a sequence of dictionaries to a sequence of tuples""" - if third_dim: - third_dim_key = THIRD_DIM_MAP[third_dim] - return ((point['lat'], point['lng'], point[third_dim_key]) for point in coordinates) - - return ((point['lat'], point['lng']) for point in coordinates) diff --git a/python/setup.py b/python/setup.py deleted file mode 100644 index ff3a2c0..0000000 --- a/python/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from setuptools import setup -from setuptools.command.sdist import sdist as _sdist -import shutil -from os import path -import io - -this_directory = path.abspath(path.dirname(__file__)) -with io.open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: - long_description = f.read() - - -class sdist(_sdist): - def run(self): - shutil.copy('../LICENSE', 'LICENSE') - _sdist.run(self) - - -setup( - name='flexpolyline', - description='Flexible Polyline encoding: a lossy compressed representation of a list of coordinate pairs or triples', - long_description=long_description, - long_description_content_type='text/markdown', - version='0.1.0', - author='HERE Europe B.V.', - url='https://here.com', - packages=['flexpolyline'], - # SPDX-License-Identifier: MIT - license='MIT', - classifiers=[ - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'License :: OSI Approved :: MIT License' - ], - project_urls={ - 'Source': 'https://github.com/heremaps/flexible-polyline.git' - }, - test_suite="test_flexpolyline", - cmdclass={'sdist': sdist} -) diff --git a/python/test_flexpolyline.py b/python/test_flexpolyline.py deleted file mode 100644 index e43c14a..0000000 --- a/python/test_flexpolyline.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (C) 2019 HERE Europe B.V. -# Licensed under MIT, see full license in LICENSE -# SPDX-License-Identifier: MIT -# License-Filename: LICENSE - -import unittest - -import flexpolyline as fp - - -class TestFlexPolyline(unittest.TestCase): - def test_encode1(self): - input = [ - (50.1022829, 8.6982122), - (50.1020076, 8.6956695), - (50.1006313, 8.6914960), - (50.0987800, 8.6875156), - ] - res = fp.encode(input) - expected = "BFoz5xJ67i1B1B7PzIhaxL7Y" - - self.assertEqual(res, expected) - - def test_dict_encode(self): - input = [ - {'lat': 50.1022829, 'lng': 8.6982122}, - {'lat': 50.1020076, 'lng': 8.6956695}, - {'lat': 50.1006313, 'lng': 8.6914960}, - {'lat': 50.0987800, 'lng': 8.6875156} - ] - res = fp.dict_encode(input) - expected = "BFoz5xJ67i1B1B7PzIhaxL7Y" - - self.assertEqual(res, expected) - - def test_encode_alt(self): - input = [ - (50.1022829, 8.6982122, 10), - (50.1020076, 8.6956695, 20), - (50.1006313, 8.6914960, 30), - (50.0987800, 8.6875156, 40), - ] - res = fp.encode(input, third_dim=fp.ALTITUDE) - expected = "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU" - - self.assertEqual(res, expected) - - def test_encode2(self): - input = [ - [52.5199356, 13.3866272], - [52.5100899, 13.2816896], - [52.4351807, 13.1935196], - [52.4107285, 13.1964502], - [52.38871, 13.1557798], - [52.3727798, 13.1491003], - [52.3737488, 13.1154604], - [52.3875198, 13.0872202], - [52.4029388, 13.0706196], - [52.4105797, 13.0755529], - ] - - res = fp.encode(input) - expected = "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e" - - self.assertEqual(res, expected) - - def assertAlmostEqualSequence(self, first, second, places=None): - for row1, row2 in zip(first, second): - for a, b in zip(row1, row2): - self.assertAlmostEqual(a, b, places=places) - - def assertAlmostEqualDictSequence(self, first, second, places=None): - for row1, row2 in zip(first, second): - for k, a in row1.items(): - self.assertAlmostEqual(a, row2[k], places=places) - - def test_iter_decode1(self): - polyline = list(p for p in fp.iter_decode("BFoz5xJ67i1B1B7PzIhaxL7Y")) - expected = [ - (50.10228, 8.69821), - (50.10201, 8.69567), - (50.10063, 8.69150), - (50.09878, 8.68752) - ] - self.assertAlmostEqualSequence(polyline, expected, places=7) - - def test_iter_decode_fails(self): - with self.assertRaises(ValueError): - list(fp.iter_decode("BFoz5xJ67i1B1B7PzIhaxL7")) - - with self.assertRaises(ValueError): - list(fp.iter_decode("CFoz5xJ67i1B1B7PzIhaxL7")) - - def test_dict_decode(self): - polyline = fp.dict_decode("BlBoz5xJ67i1BU1B7PUzIhaUxL7YU") - expected = [ - {'lat': 50.10228, 'lng': 8.69821, 'alt': 10}, - {'lat': 50.10201, 'lng': 8.69567, 'alt': 20}, - {'lat': 50.10063, 'lng': 8.69150, 'alt': 30}, - {'lat': 50.09878, 'lng': 8.68752, 'alt': 40} - ] - self.assertAlmostEqualDictSequence(polyline, expected, places=7) - - def test_iter_decode2(self): - polyline = list(fp.iter_decode("BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e")) - expected = [ - (52.51994, 13.38663), - (52.51009, 13.28169), - (52.43518, 13.19352), - (52.41073, 13.19645), - (52.38871, 13.15578), - (52.37278, 13.14910), - (52.37375, 13.11546), - (52.38752, 13.08722), - (52.40294, 13.07062), - (52.41058, 13.07555), - ] - self.assertAlmostEqualSequence(polyline, expected, places=7) - - def test_get_third_dimension(self): - self.assertEqual(fp.get_third_dimension("BFoz5xJ67i1BU"), fp.ABSENT) - self.assertEqual(fp.get_third_dimension("BVoz5xJ67i1BU"), fp.LEVEL) - self.assertEqual(fp.get_third_dimension("BlBoz5xJ67i1BU"), fp.ALTITUDE) - self.assertEqual(fp.get_third_dimension("B1Boz5xJ67i1BU"), fp.ELEVATION) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/rust/Cargo.toml b/rust/Cargo.toml deleted file mode 100644 index 4f85752..0000000 --- a/rust/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "flexpolyline" -version = "0.1.0" -description = "Flexible Polyline encoding: a lossy compressed representation of a list of coordinate pairs or triples" -authors = ["HERE Europe B.V."] -repository = "https://github.com/heremaps/flexible-polyline.git" -license = "MIT" -keywords = ["polyline", "encoding"] -edition = "2018" - -[dependencies] - -[dev-dependencies] -rand = "0.6.5" - -[[bin]] -name = "flexpolyline" -path = "src/cli.rs" diff --git a/rust/examples/random.rs b/rust/examples/random.rs deleted file mode 100644 index e542a47..0000000 --- a/rust/examples/random.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Example which generates random polylines covering all possible cases. - -use rand::prelude::*; - -fn main() { - let mut rng = rand::thread_rng(); - let range_lat = 180 * 10_i64.pow(15); - let range_lon = 90 * 10_i64.pow(15); - let range_z = 1000 * 10_i64.pow(14); - for &divisor in [ - 1, - 10_i64.pow(4), - 10_i64.pow(8), - 10_i64.pow(12), - 10_i64.pow(17), - 10_i64.pow(18), - ] - .iter() - { - for num_coords in 1..5 { - for prec2d in 0..=15 { - for &type3d in [ - flexpolyline::Type3d::Level, - flexpolyline::Type3d::Altitude, - flexpolyline::Type3d::Elevation, - flexpolyline::Type3d::Reserved1, - flexpolyline::Type3d::Reserved2, - flexpolyline::Type3d::Custom1, - flexpolyline::Type3d::Custom2, - ] - .iter() - { - let polyline = flexpolyline::Polyline::Data3d { - precision2d: flexpolyline::Precision::from_u32(prec2d).unwrap(), - precision3d: flexpolyline::Precision::from_u32(15 - prec2d).unwrap(), - type3d, - coordinates: (0..num_coords) - .map(|_| { - ( - (rng.gen_range(-range_lat, range_lat) / divisor) as f64 - / 10_i64.pow(15) as f64, - (rng.gen_range(-range_lon, range_lon) / divisor) as f64 - / 10_i64.pow(15) as f64, - (rng.gen_range(-range_z, range_z) / divisor) as f64 - / 10_i64.pow(14) as f64, - ) - }) - .collect(), - }; - println!("{:.15}", polyline); - } - - let polyline = flexpolyline::Polyline::Data2d { - precision2d: flexpolyline::Precision::from_u32(prec2d).unwrap(), - coordinates: (0..num_coords) - .map(|_| { - ( - (rng.gen_range(-range_lat, range_lat) / divisor) as f64 - / 10_i64.pow(15) as f64, - (rng.gen_range(-range_lon, range_lon) / divisor) as f64 - / 10_i64.pow(15) as f64, - ) - }) - .collect(), - }; - println!("{:.15}", polyline); - } - } - } -} diff --git a/rust/src/cli.rs b/rust/src/cli.rs deleted file mode 100644 index ab8fc3c..0000000 --- a/rust/src/cli.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::io::BufRead; -use std::str::FromStr; - -fn remove_decoration<'a>(x: &'a str, prefix: &str, suffix: &str) -> &'a str { - if !x.starts_with(prefix) || !x.ends_with(suffix) { - panic!("{}{} missing", prefix, suffix); - } - &x[prefix.len()..x.len() - suffix.len()] -} - -fn from_str(data: &str) -> flexpolyline::Polyline { - let parse_precision = |x: Option<&str>| { - let prec_u32 = u32::from_str(x.expect("Precision missing")) - .unwrap_or_else(|e| panic!("Precision not parsable: {}", e)); - flexpolyline::Precision::from_u32(prec_u32) - .unwrap_or_else(|| panic!("Precision outside of supported range: {}", prec_u32)) - }; - let parse_3d_type = |x: Option<&str>| { - let value_u32 = u32::from_str(x.expect("Type3d missing")) - .unwrap_or_else(|e| panic!("Type3d not parsable: {}", e)); - match value_u32 { - 1 => flexpolyline::Type3d::Level, - 2 => flexpolyline::Type3d::Altitude, - 3 => flexpolyline::Type3d::Elevation, - 4 => flexpolyline::Type3d::Reserved1, - 5 => flexpolyline::Type3d::Reserved2, - 6 => flexpolyline::Type3d::Custom1, - 7 => flexpolyline::Type3d::Custom2, - _ => panic!("Unexpected 3d type: {}", value_u32), - } - }; - - let data = remove_decoration(data, "{", "}"); - let mut split = data.split("; "); - let header = remove_decoration(split.next().expect("header not found"), "(", ")"); - let mut components = header.split(", "); - let precision2d = parse_precision(components.next()); - let result = match components.next() { - None => { - let data = remove_decoration(split.next().expect("data not found"), "[(", "), ]"); - let coordinates = data - .split("), (") - .filter_map(|x| { - if x.is_empty() { - None - } else { - let mut coord = x.split(", "); - let lat = f64::from_str(coord.next().expect("Missing latitude")) - .unwrap_or_else(|e| panic!("latitude not parseable: {}", e)); - let lon = f64::from_str(coord.next().expect("Missing longitude")) - .unwrap_or_else(|e| panic!("longitude not parseable: {}", e)); - if let Some(trail) = coord.next() { - panic!("Too many components in 2d coordinate: {}", trail); - } - Some((lat, lon)) - } - }) - .collect(); - flexpolyline::Polyline::Data2d { - precision2d, - coordinates, - } - } - Some(precision) => { - let precision3d = parse_precision(Some(precision)); - let type3d = parse_3d_type(components.next()); - if let Some(trail) = components.next() { - panic!("Too many components in header: {}", trail); - } - - let data = remove_decoration(split.next().expect("data not found"), "[(", "), ]"); - let coordinates = data - .split("), (") - .filter_map(|x| { - if x.is_empty() { - None - } else { - let mut coord = x.split(", "); - let lat = f64::from_str(coord.next().expect("Missing latitude")) - .unwrap_or_else(|e| panic!("latitude not parseable: {}", e)); - let lon = f64::from_str(coord.next().expect("Missing longitude")) - .unwrap_or_else(|e| panic!("longitude not parseable: {}", e)); - let z = f64::from_str(coord.next().expect("Missing 3d component")) - .unwrap_or_else(|e| panic!("3d component not parseable: {}", e)); - if let Some(trail) = coord.next() { - panic!("Too many components in 3d coordinate: {}", trail); - } - Some((lat, lon, z)) - } - }) - .collect(); - flexpolyline::Polyline::Data3d { - precision2d, - precision3d, - type3d, - coordinates, - } - } - }; - - if let Some(trail) = components.next() { - panic!("Too many components in data: {}", trail); - } - - result -} - -fn main() { - let args: Vec = std::env::args().collect(); - if args.len() != 2 || (args[1] != "encode" && args[1] != "decode") { - eprintln!("Usage: flexpolyline encode|decode"); - eprintln!(" input: stdin"); - eprintln!(" output: stdout"); - std::process::exit(1); - } - - let stdin = std::io::stdin(); - - if args[1] == "encode" { - for line in stdin.lock().lines() { - let input = line.unwrap(); - let polyline = from_str(&input); - println!( - "{}", - polyline - .encode() - .unwrap_or_else(|e| panic!("Failed to encode {}: {}", input, e)) - ); - } - } else { - for line in stdin.lock().lines() { - let input = line.unwrap(); - let polyline = flexpolyline::Polyline::decode(&input) - .unwrap_or_else(|e| panic!("Failed to decode {}: {}", input, e)); - println!("{:.15}", polyline); - } - } -} diff --git a/rust/src/lib.rs b/rust/src/lib.rs deleted file mode 100644 index 681fe4d..0000000 --- a/rust/src/lib.rs +++ /dev/null @@ -1,823 +0,0 @@ -//! # Flexible Polyline encoding -//! -//! The flexible polyline encoding is a lossy compressed representation of a list of coordinate -//! pairs or coordinate triples. It achieves that by: -//! -//! 1. Reducing the decimal digits of each value. -//! 2. Encoding only the offset from the previous point. -//! 3. Using variable length for each coordinate delta. -//! 4. Using 64 URL-safe characters to display the result. -//! -//! The encoding is a variant of [Encoded Polyline Algorithm Format]. The advantage of this encoding -//! over the original are the following: -//! -//! * Output string is composed by only URL-safe characters, i.e. may be used without URL encoding -//! as query parameters. -//! * Floating point precision is configurable: This allows to represent coordinates with precision -//! up to microns (5 decimal places allow meter precision only). -//! * It allows to encode a 3rd dimension with a given precision, which may be a level, altitude, -//! elevation or some other custom value. -//! -//! ## Specification -//! -//! See [Specification]. -//! -//! [Encoded Polyline Algorithm Format]: https://developers.google.com/maps/documentation/utilities/polylinealgorithm -//! [Specification]: https://github.com/heremaps/flexible-polyline#specifications -//! -//! ## Example -//! -//! ```rust -//! use flexpolyline::{Polyline, Precision}; -//! -//! // encode -//! let coordinates = vec![ -//! (50.1022829, 8.6982122), -//! (50.1020076, 8.6956695), -//! (50.1006313, 8.6914960), -//! (50.0987800, 8.6875156), -//! ]; -//! -//! let polyline = Polyline::Data2d { -//! coordinates, -//! precision2d: Precision::Digits5, -//! }; -//! -//! let encoded = polyline.encode().unwrap(); -//! assert_eq!(encoded, "BFoz5xJ67i1B1B7PzIhaxL7Y"); -//! -//! // decode -//! let decoded = Polyline::decode("BFoz5xJ67i1B1B7PzIhaxL7Y").unwrap(); -//! assert_eq!( -//! decoded, -//! Polyline::Data2d { -//! coordinates: vec![ -//! (50.10228, 8.69821), -//! (50.10201, 8.69567), -//! (50.10063, 8.69150), -//! (50.09878, 8.68752) -//! ], -//! precision2d: Precision::Digits5 -//! } -//! ); -//! ``` - -#![doc(html_playground_url = "https://play.rust-lang.org/")] -#![deny(warnings, missing_docs)] -#![allow(clippy::unreadable_literal)] - -/// Coordinate precision in the polyline -/// -/// Represents how many digits are to be encoded after the decimal point, e.g. -/// precision 3 would encode 4.456787 as 4.457. -/// -/// Supported values: `[0,16)` -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum Precision { - /// 0 decimal digits - Digits0 = 0, - /// 1 decimal digits - Digits1 = 1, - /// 2 decimal digits - Digits2 = 2, - /// 3 decimal digits - Digits3 = 3, - /// 4 decimal digits - Digits4 = 4, - /// 5 decimal digits - Digits5 = 5, - /// 6 decimal digits - Digits6 = 6, - /// 7 decimal digits - Digits7 = 7, - /// 8 decimal digits - Digits8 = 8, - /// 9 decimal digits - Digits9 = 9, - /// 10 decimal digits - Digits10 = 10, - /// 11 decimal digits - Digits11 = 11, - /// 12 decimal digits - Digits12 = 12, - /// 13 decimal digits - Digits13 = 13, - /// 14 decimal digits - Digits14 = 14, - /// 15 decimal digits - Digits15 = 15, -} - -impl Precision { - /// Converts `u32` to precision. - pub fn from_u32(digits: u32) -> Option { - match digits { - 0 => Some(Precision::Digits0), - 1 => Some(Precision::Digits1), - 2 => Some(Precision::Digits2), - 3 => Some(Precision::Digits3), - 4 => Some(Precision::Digits4), - 5 => Some(Precision::Digits5), - 6 => Some(Precision::Digits6), - 7 => Some(Precision::Digits7), - 8 => Some(Precision::Digits8), - 9 => Some(Precision::Digits9), - 10 => Some(Precision::Digits10), - 11 => Some(Precision::Digits11), - 12 => Some(Precision::Digits12), - 13 => Some(Precision::Digits13), - 14 => Some(Precision::Digits14), - 15 => Some(Precision::Digits15), - _ => None, - } - } - - /// Converts precision to `u32`. - pub fn to_u32(self) -> u32 { - self as u32 - } -} - -/// Informs about the type of the 3rd dimension of a 3D coordinate vector -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Type3d { - /// E.g. floor of a building - Level = 1, - /// E.g. altitude (in the air) relative to ground level or mean sea level - Altitude = 2, - /// E.g. elevation above mean-sea-level - Elevation = 3, - /// Reserved for future types - Reserved1 = 4, - /// Reserved for future types - Reserved2 = 5, - /// Reserved for custom types - Custom1 = 6, - /// Reserved for custom types - Custom2 = 7, -} - -/// 2- or 3-dimensional polyline -#[derive(Debug, Clone, PartialEq)] -pub enum Polyline { - /// 2-dimensional polyline - Data2d { - /// List of 2D coordinates making up this polyline - coordinates: Vec<(f64, f64)>, - /// Precision of the coordinates (e.g. used for encoding, - /// or to report the precision supplied in encoded data) - precision2d: Precision, - }, - /// 3-dimensional polyline - Data3d { - /// List of 3D coordinates making up this polyline - coordinates: Vec<(f64, f64, f64)>, - /// Precision of the 2D part of the coordinates (e.g. used for encoding, - /// or to report the precision supplied in encoded data) - precision2d: Precision, - /// Precision of the 3D part of the coordinates (e.g. used for encoding, - /// or to report the precision supplied in encoded data) - precision3d: Precision, - /// Type of the 3D component - type3d: Type3d, - }, -} - -impl std::fmt::Display for Polyline { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let prec = f.precision().unwrap_or(6); - match self { - Polyline::Data2d { - coordinates, - precision2d, - } => { - write!(f, "{{({}); [", precision2d.to_u32())?; - for coord in coordinates { - write!( - f, - "({:.*}, {:.*}), ", - prec as usize, coord.0, prec as usize, coord.1 - )?; - } - write!(f, "]}}")?; - } - Polyline::Data3d { - coordinates, - precision2d, - precision3d, - type3d, - } => { - write!( - f, - "{{({}, {}, {}); [", - precision2d.to_u32(), - precision3d.to_u32(), - *type3d as usize - )?; - for coord in coordinates { - write!( - f, - "({:.*}, {:.*}, {:.*}), ", - prec as usize, coord.0, prec as usize, coord.1, prec as usize, coord.2 - )?; - } - write!(f, "]}}")?; - } - } - Ok(()) - } -} - -/// Error reported when encoding or decoding polylines -#[derive(Debug, PartialEq, Eq)] -pub enum Error { - /// Data is encoded with unsupported version - UnsupportedVersion, - /// Precision is not supported by encoding - InvalidPrecision, - /// Encoding is corrupt - InvalidEncoding, - #[doc(hidden)] - __Nonexhaustive, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Error::UnsupportedVersion => write!(f, "UnsupportedVersion"), - Error::InvalidPrecision => write!(f, "InvalidPrecision"), - Error::InvalidEncoding => write!(f, "InvalidEncoding"), - Error::__Nonexhaustive => panic!(), - } - } -} - -impl std::error::Error for Error {} - -impl Polyline { - /// Encodes a polyline into a string. - /// - /// The precision of the polyline is used to round coordinates, so the transformation is lossy - /// in nature. - pub fn encode(&self) -> Result { - match self { - Polyline::Data2d { - coordinates, - precision2d, - } => encode_2d(&coordinates, precision2d.to_u32()), - Polyline::Data3d { - coordinates, - precision2d, - precision3d, - type3d, - } => encode_3d( - coordinates, - precision2d.to_u32(), - precision3d.to_u32(), - *type3d as u32, - ), - } - } - - /// Decodes an encoded polyline. - pub fn decode>(encoded: S) -> Result { - let mut bytes = encoded.as_ref().bytes(); - - let (precision2d, precision3d, type3d) = decode_header(&mut bytes)?; - - let type3d = match type3d { - 1 => Some(Type3d::Level), - 2 => Some(Type3d::Altitude), - 3 => Some(Type3d::Elevation), - 4 => Some(Type3d::Reserved1), - 5 => Some(Type3d::Reserved2), - 6 => Some(Type3d::Custom1), - 7 => Some(Type3d::Custom2), - 0 => None, - _ => panic!(), // impossible, we only decoded 3 bits - }; - - if let Some(type3d) = type3d { - let coordinates = decode3d(bytes, precision2d, precision3d)?; - Ok(Polyline::Data3d { - coordinates, - precision2d: Precision::from_u32(precision2d).ok_or(Error::InvalidPrecision)?, - precision3d: Precision::from_u32(precision3d).ok_or(Error::InvalidPrecision)?, - type3d, - }) - } else { - let coordinates = decode2d(bytes, precision2d)?; - Ok(Polyline::Data2d { - coordinates, - precision2d: Precision::from_u32(precision2d).ok_or(Error::InvalidPrecision)?, - }) - } - } -} - -fn precision_to_scale(precision: u32) -> impl Fn(f64) -> i64 { - let scale = 10_u64.pow(precision) as f64; - move |value: f64| (value * scale).round() as i64 -} - -fn precision_to_inverse_scale(precision: u32) -> impl Fn(i64) -> f64 { - let scale = 10_u64.pow(precision) as f64; - move |value: i64| value as f64 / scale -} - -fn encode_header( - precision2d: u32, - precision3d: u32, - type3d: u32, - result: &mut String, -) -> Result<(), Error> { - if precision2d > 15 || precision3d > 15 { - return Err(Error::InvalidPrecision); - } - var_encode_u64(1, result); // Version 1 - let header = (precision3d << 7) | (type3d << 4) | precision2d; - var_encode_u64(u64::from(header), result); - Ok(()) -} - -fn encode_2d(coords: &[(f64, f64)], precision2d: u32) -> Result { - let mut result = String::with_capacity((coords.len() * 2) + 2); - - encode_header(precision2d, 0, 0, &mut result)?; - let scale2d = precision_to_scale(precision2d); - - let mut last_coord = (0, 0); - for coord in coords { - let scaled_coord = (scale2d(coord.0), scale2d(coord.1)); - var_encode_i64(scaled_coord.0 - last_coord.0, &mut result); - var_encode_i64(scaled_coord.1 - last_coord.1, &mut result); - last_coord = scaled_coord; - } - - Ok(result) -} - -fn encode_3d( - coords: &[(f64, f64, f64)], - precision2d: u32, - precision3d: u32, - type3d: u32, -) -> Result { - let mut result = String::with_capacity((coords.len() * 3) + 2); - - encode_header(precision2d, precision3d, type3d, &mut result)?; - let scale2d = precision_to_scale(precision2d); - let scale3d = precision_to_scale(precision3d); - - let mut last_coord = (0, 0, 0); - for coord in coords { - let scaled_coord = (scale2d(coord.0), scale2d(coord.1), scale3d(coord.2)); - var_encode_i64(scaled_coord.0 - last_coord.0, &mut result); - var_encode_i64(scaled_coord.1 - last_coord.1, &mut result); - var_encode_i64(scaled_coord.2 - last_coord.2, &mut result); - last_coord = scaled_coord; - } - - Ok(result) -} - -fn var_encode_i64(value: i64, result: &mut String) { - // make room on lowest bit - let mut encoded = (value << 1) as u64; - - // invert bits if the value is negative - if value < 0 { - encoded = !encoded; - } - - var_encode_u64(encoded, result); -} - -fn var_encode_u64(mut value: u64, result: &mut String) { - const ENCODING_TABLE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - - // var-length encode the number in chunks of 5 bits starting with the least significant - // to the most significant - while value > 0x1F { - let pos = (value & 0x1F) | 0x20; - let c = ENCODING_TABLE.as_bytes()[pos as usize] as char; - result.push(c); - value >>= 5; - } - let c = ENCODING_TABLE.as_bytes()[value as usize] as char; - result.push(c); -} - -fn decode_header>(bytes: &mut I) -> Result<(u32, u32, u32), Error> { - let version = var_decode_u64(bytes)?; - - if version != 1 { - return Err(Error::UnsupportedVersion); - } - - let header = var_decode_u64(bytes)?; - - if header >= (1_u64 << 11) { - return Err(Error::InvalidEncoding); - } - let precision2d = (header & 15) as u32; - let type3d = ((header >> 4) & 7) as u32; - let precision3d = ((header >> 7) & 15) as u32; - - Ok((precision2d, precision3d, type3d)) -} - -fn decode2d>( - mut bytes: I, - precision2d: u32, -) -> Result, Error> { - let mut result = Vec::with_capacity(bytes.len() / 2); - let scale2d = precision_to_inverse_scale(precision2d); - let mut last_coord = (0, 0); - while bytes.len() > 0 { - let delta = (var_decode_i64(&mut bytes)?, var_decode_i64(&mut bytes)?); - last_coord = (last_coord.0 + delta.0, last_coord.1 + delta.1); - - result.push((scale2d(last_coord.0), scale2d(last_coord.1))); - } - Ok(result) -} - -fn decode3d>( - mut bytes: I, - precision2d: u32, - precision3d: u32, -) -> Result, Error> { - let mut result = Vec::with_capacity(bytes.len() / 2); - let scale2d = precision_to_inverse_scale(precision2d); - let scale3d = precision_to_inverse_scale(precision3d); - let mut last_coord = (0, 0, 0); - while bytes.len() > 0 { - let delta = ( - var_decode_i64(&mut bytes)?, - var_decode_i64(&mut bytes)?, - var_decode_i64(&mut bytes)?, - ); - last_coord = ( - last_coord.0 + delta.0, - last_coord.1 + delta.1, - last_coord.2 + delta.2, - ); - - result.push(( - scale2d(last_coord.0), - scale2d(last_coord.1), - scale3d(last_coord.2), - )); - } - Ok(result) -} - -fn var_decode_i64>(bytes: &mut I) -> Result { - match var_decode_u64(bytes) { - Ok(mut value) => { - let negative = (value & 1) != 0; - value >>= 1; - if negative { - value = !value; - } - Ok(value as i64) - } - Err(err) => Err(err), - } -} - -fn var_decode_u64>(bytes: &mut I) -> Result { - #[rustfmt::skip] - const DECODING_TABLE: &[i8] = &[ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, 62, -1, -1, 52, 53, - 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, - -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, - 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, - 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, - 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, - ]; - - let mut result: u64 = 0; - let mut shift = 0; - - for byte in bytes { - let value = DECODING_TABLE[byte as usize]; - if value < 0 { - return Err(Error::InvalidEncoding); - } - - let value = value as u64; - result |= (value & 0x1F) << shift; - - if (value & 0x20) == 0 { - return Ok(result); - } - - shift += 5; - - if shift >= 64 { - return Err(Error::InvalidEncoding); - } - } - - Err(Error::InvalidEncoding) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_var_encode_i64() { - let mut buf = String::new(); - var_encode_i64(-17998321, &mut buf); - assert_eq!(buf, "h_wqiB"); - } - - #[test] - fn test_encode_2d_example_1() { - let coordinates = vec![ - (50.1022829, 8.6982122), - (50.1020076, 8.6956695), - (50.1006313, 8.6914960), - (50.0987800, 8.6875156), - ]; - - let expected = "BFoz5xJ67i1B1B7PzIhaxL7Y"; - assert_eq!( - &Polyline::Data2d { - coordinates, - precision2d: Precision::Digits5 - } - .encode() - .unwrap(), - expected - ); - } - - #[test] - fn test_encode_2d_example_2() { - let coordinates = vec![ - (52.5199356, 13.3866272), - (52.5100899, 13.2816896), - (52.4351807, 13.1935196), - (52.4107285, 13.1964502), - (52.3887100, 13.1557798), - (52.3727798, 13.1491003), - (52.3737488, 13.1154604), - (52.3875198, 13.0872202), - (52.4029388, 13.0706196), - (52.4105797, 13.0755529), - ]; - - let expected = "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e"; - assert_eq!( - &Polyline::Data2d { - coordinates, - precision2d: Precision::Digits5 - } - .encode() - .unwrap(), - expected - ); - } - - #[test] - fn test_encode_3d_example_1() { - let coordinates = vec![ - (50.1022829, 8.6982122, 10.0), - (50.1020076, 8.6956695, 20.0), - (50.1006313, 8.6914960, 30.0), - (50.0987800, 8.6875156, 40.0), - ]; - - let expected = "BVoz5xJ67i1BU1B7PUzIhaUxL7YU"; - assert_eq!( - &Polyline::Data3d { - coordinates, - precision2d: Precision::Digits5, - precision3d: Precision::Digits0, - type3d: Type3d::Level - } - .encode() - .unwrap(), - expected - ); - } - - #[test] - fn test_var_decode_i64() -> Result<(), Error> { - let mut bytes = "h_wqiB".bytes(); - let res = var_decode_i64(&mut bytes)?; - assert_eq!(res, -17998321); - let res = var_decode_i64(&mut bytes); - assert!(res.is_err()); - - let mut bytes = "hhhhhhhhhhhhhhhhhhh".bytes(); - let res = var_decode_i64(&mut bytes); - assert!(res.is_err()); - Ok(()) - } - - #[test] - fn test_decode_2d_example_1() -> Result<(), Error> { - let polyline = Polyline::decode("BFoz5xJ67i1B1B7PzIhaxL7Y")?; - let expected = "{(5); [\ - (50.102280, 8.698210), \ - (50.102010, 8.695670), \ - (50.100630, 8.691500), \ - (50.098780, 8.687520), \ - ]}"; - let result = format!("{:.6}", polyline); - assert_eq!(expected, result); - Ok(()) - } - - #[test] - fn test_decode_2d_example_2() -> Result<(), Error> { - let polyline = - Polyline::decode("BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e")?; - let expected = "{(5); [\ - (52.519940, 13.386630), \ - (52.510090, 13.281690), \ - (52.435180, 13.193520), \ - (52.410730, 13.196450), \ - (52.388710, 13.155780), \ - (52.372780, 13.149100), \ - (52.373750, 13.115460), \ - (52.387520, 13.087220), \ - (52.402940, 13.070620), \ - (52.410580, 13.075550), \ - ]}"; - - let result = format!("{:.6}", polyline); - assert_eq!(expected, result); - Ok(()) - } - - #[test] - fn test_decode_3d_example_1() -> Result<(), Error> { - let polyline = Polyline::decode("BVoz5xJ67i1BU1B7PUzIhaUxL7YU")?; - let expected = "{(5, 0, 1); [\ - (50.102280, 8.698210, 10.000000), \ - (50.102010, 8.695670, 20.000000), \ - (50.100630, 8.691500, 30.000000), \ - (50.098780, 8.687520, 40.000000), \ - ]}"; - - let result = format!("{:.6}", polyline); - assert_eq!(expected, result); - Ok(()) - } - - #[test] - #[allow(clippy::zero_prefixed_literal)] - fn test_encode_decode_2d() -> Result<(), Error> { - let coordinate_values: Vec<(u64, u64)> = vec![ - (96821474666297905, 78334196549606266), - (29405294060895017, 70361389340728572), - (16173544634348013, 17673855782924183), - (22448654820449524, 13005139703027850), - (73351231936757857, 78298027377720633), - (78008331957098324, 04847613123220218), - (62755680515396509, 49165433608990700), - (93297154866561429, 52373802822465027), - (89973844644540399, 75975762025877533), - (48555821719956867, 31591090068957813), - ]; - - for precision2d in 0..=15 { - let to_f64 = |value: &(u64, u64)| { - ( - value.0 as f64 / 10_u64.pow(15) as f64, - value.1 as f64 / 10_u64.pow(15) as f64, - ) - }; - - let to_rounded_f64 = |value: &(u64, u64)| { - let value = to_f64(value); - let scale = 10_u64.pow(precision2d) as f64; - ( - (value.0 * scale).round() / scale, - (value.1 * scale).round() / scale, - ) - }; - - let expected = format!( - "{:.*}", - precision2d as usize + 1, - Polyline::Data2d { - coordinates: coordinate_values.iter().map(to_rounded_f64).collect(), - precision2d: Precision::from_u32(precision2d).unwrap(), - } - ); - - let encoded = &Polyline::Data2d { - coordinates: coordinate_values.iter().map(to_f64).collect(), - precision2d: Precision::from_u32(precision2d).unwrap(), - } - .encode()?; - - let polyline = Polyline::decode(&encoded)?; - let result = format!("{:.*}", precision2d as usize + 1, polyline); - assert_eq!(expected, result); - } - - Ok(()) - } - - #[test] - #[allow(clippy::zero_prefixed_literal)] - fn test_encode_decode_3d() -> Result<(), Error> { - let coordinate_values: Vec<(u64, u64, u64)> = vec![ - (96821474666297905, 78334196549606266, 23131023979661380), - (29405294060895017, 70361389340728572, 81917934930416924), - (16173544634348013, 17673855782924183, 86188502094968953), - (22448654820449524, 13005139703027850, 68774670569614983), - (73351231936757857, 78298027377720633, 52078352171243855), - (78008331957098324, 04847613123220218, 06550838806837986), - (62755680515396509, 49165433608990700, 39041897671300539), - (93297154866561429, 52373802822465027, 67310807938230681), - (89973844644540399, 75975762025877533, 66789448009436096), - (48555821719956867, 31591090068957813, 49203621966471323), - ]; - - let precision2d = 5; - for precision3d in 0..=15 { - for type3d in &[ - Type3d::Level, - Type3d::Altitude, - Type3d::Elevation, - Type3d::Reserved1, - Type3d::Reserved2, - Type3d::Custom1, - Type3d::Custom2, - ] { - let to_f64 = |value: &(u64, u64, u64)| { - ( - value.0 as f64 / 10_u64.pow(15) as f64, - value.1 as f64 / 10_u64.pow(15) as f64, - value.2 as f64 / 10_u64.pow(15) as f64, - ) - }; - - let to_rounded_f64 = |value: &(u64, u64, u64)| { - let value = to_f64(value); - let scale2d = 10_u64.pow(precision2d) as f64; - let scale3d = 10_u64.pow(precision3d) as f64; - ( - (value.0 * scale2d).round() / scale2d, - (value.1 * scale2d).round() / scale2d, - (value.2 * scale3d).round() / scale3d, - ) - }; - - let expected = format!( - "{:.*}", - precision2d.max(precision3d) as usize + 1, - Polyline::Data3d { - coordinates: coordinate_values.iter().map(to_rounded_f64).collect(), - precision2d: Precision::from_u32(precision2d).unwrap(), - precision3d: Precision::from_u32(precision3d).unwrap(), - type3d: *type3d, - } - ); - - let encoded = Polyline::Data3d { - coordinates: coordinate_values.iter().map(to_f64).collect(), - precision2d: Precision::from_u32(precision2d).unwrap(), - precision3d: Precision::from_u32(precision3d).unwrap(), - type3d: *type3d, - } - .encode()?; - - let polyline = Polyline::decode(&encoded)?; - let result = format!("{:.*}", precision2d.max(precision3d) as usize + 1, polyline); - assert_eq!(expected, result); - } - } - - Ok(()) - } -}