From 81fc52ac903846e31e31fca7b58a016f9bbd4766 Mon Sep 17 00:00:00 2001 From: Mykhailo Kuchma Date: Wed, 5 Mar 2025 20:51:29 +0100 Subject: [PATCH] Add path tiling utilities Introduce the iterator types used to traverse and slice a GeoCoordinates sequence to tiles: * Add TilingIterator to convert the GeoCoordinates to a TileKeys * Add AdjacentPairIterator to iterate over the adjacent pairs * Add LineSliceIterator type to slice the path from point A to point B * Add various helper functions to ease the iterators creation Add a Bresenham's line algorithm used to generate tiles over a grid Relates-To: OCMAM-461 Signed-off-by: Mykhailo Kuchma --- olp-cpp-sdk-core/CMakeLists.txt | 1 + .../include/olp/core/geo/tiling/PathTiling.h | 460 ++++++++++++++++++ olp-cpp-sdk-core/tests/CMakeLists.txt | 1 + .../tests/geo/tiling/PathTilingTest.cpp | 109 +++++ 4 files changed, 571 insertions(+) create mode 100644 olp-cpp-sdk-core/include/olp/core/geo/tiling/PathTiling.h create mode 100644 olp-cpp-sdk-core/tests/geo/tiling/PathTilingTest.cpp diff --git a/olp-cpp-sdk-core/CMakeLists.txt b/olp-cpp-sdk-core/CMakeLists.txt index 7098f10df..08cb14c6d 100644 --- a/olp-cpp-sdk-core/CMakeLists.txt +++ b/olp-cpp-sdk-core/CMakeLists.txt @@ -197,6 +197,7 @@ set(OLP_SDK_GEOTILING_HEADERS ./include/olp/core/geo/tiling/HalfQuadTreeSubdivisionScheme.h ./include/olp/core/geo/tiling/ISubdivisionScheme.h ./include/olp/core/geo/tiling/ITilingScheme.h + ./include/olp/core/geo/tiling/PathTiling.h ./include/olp/core/geo/tiling/QuadTreeSubdivisionScheme.h ./include/olp/core/geo/tiling/SubTiles.h ./include/olp/core/geo/tiling/TileKey.h diff --git a/olp-cpp-sdk-core/include/olp/core/geo/tiling/PathTiling.h b/olp-cpp-sdk-core/include/olp/core/geo/tiling/PathTiling.h new file mode 100644 index 000000000..e9ab17dc4 --- /dev/null +++ b/olp-cpp-sdk-core/include/olp/core/geo/tiling/PathTiling.h @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2025 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace olp { +namespace geo { + +namespace detail { + +/** + * @class LineEvaluator + * @brief Implements Bresenham's line algorithm with a square sliding window. + * + * This class provides a way to iterate over a line while considering a + * sliding window to enable line width. + */ +class LineEvaluator { + public: + /** + * @struct State + * @brief Holds the state of the line traversal. + */ + struct State { + int32_t x_end; + bool is_slope_reversed; + int32_t sliding_window_half_size; + int32_t delta_x; + int32_t delta_y; + int32_t y_step; + int32_t x; + int32_t y; + int32_t error; + int32_t sliding_offset_x; + int32_t sliding_offset_y; + uint32_t tile_level; + + bool operator==(const State& other) const { + return x_end == other.x_end && + is_slope_reversed == other.is_slope_reversed && + sliding_window_half_size == other.sliding_window_half_size && + delta_x == other.delta_x && delta_y == other.delta_y && + y_step == other.y_step && x == other.x && y == other.y && + error == other.error && + sliding_offset_x == other.sliding_offset_x && + sliding_offset_y == other.sliding_offset_y && + tile_level == other.tile_level; + } + }; + + /** + * @brief Evaluates the current tile key from the given state. + * + * @param state The current state of the line traversal. + * + * @return The tile key corresponding to the current position. + */ + static TileKey Value(const State& state) { + int tile_x = state.x + state.sliding_offset_x; + int tile_y = state.y + state.sliding_offset_y; + + if (state.is_slope_reversed) + std::swap(tile_x, tile_y); + + return TileKey::FromRowColumnLevel(tile_y, tile_x, state.tile_level); + } + + /** + * @brief Iterates the state towards the end of the line. + * + * @param state The current state of the line traversal. + * + * @return True if the iteration continues, false if it ends. + */ + static bool Iterate(State& state) { // NOLINT + if (state.x > state.x_end) { + return false; + } + + if (++state.sliding_offset_y > state.sliding_window_half_size) { + state.sliding_offset_y = -state.sliding_window_half_size; + if (++state.sliding_offset_x > state.sliding_window_half_size) { + state.sliding_offset_x = -state.sliding_window_half_size; + + state.error += state.delta_y; + if (state.error * 2 >= state.delta_x) { + state.y += state.y_step; + state.error -= state.delta_x; + } + + ++state.x; + } + } + + return true; + } + + /** + * @brief Initializes the line state between two tiles. + * + * @param start_tile The starting tile. + * @param end_tile The ending tile. + * @param sliding_window_half_size The half-size of the sliding window. + * + * @return The initialized line state. + */ + static State Init(const TileKey start_tile, const TileKey end_tile, + const int32_t sliding_window_half_size) { + int32_t x0 = static_cast(start_tile.Column()); + int32_t y0 = static_cast(start_tile.Row()); + int32_t x1 = static_cast(end_tile.Column()); + int32_t y1 = static_cast(end_tile.Row()); + + const uint32_t tile_level = start_tile.Level(); + + const bool should_reverse_slope = std::abs(y1 - y0) > std::abs(x1 - x0); + + if (should_reverse_slope) { + std::swap(x0, y0); + std::swap(x1, y1); + } + + if (x0 > x1) { + std::swap(x0, x1); + std::swap(y0, y1); + } + + const int32_t line_end = x1; + const int32_t delta_x = x1 - x0; + const int32_t delta_y = std::abs(y1 - y0); + const int32_t y_step = y0 > y1 ? -1 : 1; + const int32_t initial_x = x0; + const int32_t initial_y = y0; + + return State{line_end, + should_reverse_slope, + sliding_window_half_size, + delta_x, + delta_y, + y_step, + initial_x, + initial_y, + 0, + -sliding_window_half_size, + -sliding_window_half_size, + tile_level}; + } +}; +} // namespace detail + +/** + * @class TilingIterator + * @brief Iterator for transforming input coordinates into TileKeys using a + * TilingScheme. + */ +template +class TilingIterator { + public: + using value_type = TileKey; + using difference_type = std::ptrdiff_t; + using pointer = TileKey*; + using reference = TileKey&; + using iterator_category = std::forward_iterator_tag; + + /** + * @brief Constructs a TilingIterator. + * + * @param iterator The input iterator. + * @param tile_level The tile level (default: 0). + */ + explicit TilingIterator(InputIterator iterator, const uint32_t tile_level = 0) + : iterator_(iterator), tiling_scheme_(), tile_level_(tile_level) {} + + /** + * @brief Dereference operator. + * + * @return The TileKey corresponding to the current iterator position. + */ + value_type operator*() const { + return TileKeyUtils::GeoCoordinatesToTileKey( + tiling_scheme_, static_cast(*iterator_), tile_level_); + } + + TilingIterator& operator++() { + ++iterator_; + return *this; + } + + bool operator==(const TilingIterator& other) const { + return iterator_ == other.iterator_; + } + + bool operator!=(const TilingIterator& other) const { + return !(*this == other); + } + + private: + InputIterator iterator_; + TilingScheme tiling_scheme_; + uint32_t tile_level_; +}; + +/** + * @brief Helper function to create a TilingIterator. + * @param iterator The input iterator. + * @param tile_level The tile level. + * @return A new TilingIterator instance. + */ +template +auto MakeTilingIterator(InputIterator iterator, uint32_t tile_level = 0) + -> TilingIterator { + return TilingIterator(iterator, tile_level); +} + +/** + * @class AdjacentPairIterator + * @brief Iterator for iterating over adjacent pairs in a sequence. + */ +template +class AdjacentPairIterator { + public: + using value_type = std::pair; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::forward_iterator_tag; + + /** + * @brief Constructs an AdjacentPairIterator. + * + * @param segment_it The input iterator. + */ + explicit AdjacentPairIterator(InputIterator segment_it) + : current_value_(), next_it_(segment_it) {} + + /** + * @brief Constructs an AdjacentPairIterator with an initial value. + * + * @param initial_value The initial value. + * @param segment_it The input iterator. + */ + AdjacentPairIterator(typename InputIterator::value_type initial_value, + InputIterator segment_it) + : current_value_(initial_value), next_it_(segment_it) {} + + value_type operator*() const { + return std::make_pair(current_value_, *next_it_); + } + + AdjacentPairIterator& operator++() { + current_value_ = *next_it_; + ++next_it_; + return *this; + } + + bool operator==(const AdjacentPairIterator& other) const { + return next_it_ == other.next_it_; + } + + bool operator!=(const AdjacentPairIterator& other) const { + return !(*this == other); + } + + private: + typename InputIterator::value_type current_value_; + InputIterator next_it_; +}; + +/* + * @brief Creates an AdjacentPairIterator from a given iterator. + * + * @param iterator The input iterator. + * + * @return An AdjacentPairIterator initialized with the given iterator. + */ +template +auto MakeAdjacentPairIterator(InputIterator iterator) + -> AdjacentPairIterator { + return AdjacentPairIterator(iterator); +} + +/* + * @brief Creates an AdjacentPairIterator from a given iterator. + * + * @param initial_value The initial value for the adjacent pair. + * @param iterator The input iterator. + * + * @return An AdjacentPairIterator initialized with the given iterator. + */ +template +auto MakeAdjacentPairIterator(ValueType initial_value, InputIterator iterator) + -> AdjacentPairIterator { + return AdjacentPairIterator(initial_value, iterator); +} + +/* + * @brief Creates a range of AdjacentPairIterators from the given begin and end + * iterators. + * + * @param begin The beginning of the input range. + * @param end The end of the input range. + * @return A pair of AdjacentPairIterators representing the range. + */ +template > +auto MakeAdjacentPairsRange(InputIterator begin, InputIterator end) + -> std::pair { + if (begin == end) { + return std::make_pair(MakeAdjacentPairIterator(begin), + MakeAdjacentPairIterator(end)); + } + return std::make_pair(MakeAdjacentPairIterator(*begin, std::next(begin)), + MakeAdjacentPairIterator(end)); +} + +/** + * @brief An iterator that slices a line into tile segments. + * + * This iterator takes an input iterator of adjacent tile segments and + * applies a line slicing algorithm to generate individual tile keys + * along the path. + */ +template +class LineSliceIterator { + public: + using value_type = typename InputIterator::value_type; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::forward_iterator_tag; + + /** + * @brief Constructs a LineSliceIterator. + * + * @param segment_it The iterator over tile segments. + * @param line_width The width of the line in tiles. + */ + explicit LineSliceIterator(InputIterator segment_it, + const uint32_t line_width) + : segment_it_(segment_it), + half_line_width_(static_cast(line_width) / 2) {} + + TileKey operator*() { + if (!line_state_) { + TileKey begin, end; + std::tie(begin, end) = *segment_it_; + line_state_ = detail::LineEvaluator::Init(begin, end, half_line_width_); + } + return detail::LineEvaluator::Value(*line_state_); + } + + LineSliceIterator& operator++() { + if (!line_state_ || !detail::LineEvaluator::Iterate(*line_state_)) { + line_state_ = boost::none; + ++segment_it_; + } + return *this; + } + + bool operator==(const LineSliceIterator& other) const { + return segment_it_ == other.segment_it_ && + half_line_width_ == other.half_line_width_ && + line_state_ == other.line_state_; + } + + bool operator!=(const LineSliceIterator& other) const { + return !(*this == other); + } + + private: + InputIterator segment_it_; + int32_t half_line_width_; + boost::optional line_state_; +}; + +/** + * @brief Creates a LineSliceIterator from an input iterator. + * + * @param iterator The input iterator over tile segments. + * @param line_width The width of the line in tiles. + * @return A LineSliceIterator initialized with the given parameters. + */ +template +auto MakeLineSliceIterator(InputIterator iterator, const uint32_t line_width) + -> LineSliceIterator { + return LineSliceIterator(iterator, line_width); +} + +/** + * @brief Defines an iterator type that slices a tiled path into line segments. + * + * @tparam InputIterator The type of input iterator that iterates over + * geo-coordinates. + * @tparam TilingScheme The tiling scheme used to map geo-coordinates to tiles. + */ +template +using TiledPathIterator = LineSliceIterator< + AdjacentPairIterator>>; + +/** + * @brief Creates an iterator range for traversing a tiled path with a specified + * width. + * + * @note The result range has no ownership over the input range. + * + * This function constructs an iterator range that slices a path into tiles, + * using a tiling scheme and a specified path width. + * + * @tparam TilingScheme The tiling scheme used to map geo-coordinates to tiles. + * @tparam InputIterator The type of input iterator that iterates over + * geo-coordinates. + * + * @param begin The beginning iterator of the input range. + * @param end The ending iterator of the input range. + * @param level The tile level to be used for tiling. + * @param path_width The width of the path in tiles. + * + * @return A pair of iterators defining the range for traversing the tiled path. + */ +template +auto MakeTiledPathRange(InputIterator begin, InputIterator end, + const uint32_t level, const uint32_t path_width) + -> std::pair, + TiledPathIterator> { + auto adjacent_pairs_range = + MakeAdjacentPairsRange(MakeTilingIterator(begin, level), + MakeTilingIterator(end)); + return {MakeLineSliceIterator(adjacent_pairs_range.first, path_width), + MakeLineSliceIterator(adjacent_pairs_range.second, path_width)}; +} + +} // namespace geo +} // namespace olp diff --git a/olp-cpp-sdk-core/tests/CMakeLists.txt b/olp-cpp-sdk-core/tests/CMakeLists.txt index e8fb90841..447ab7c38 100644 --- a/olp-cpp-sdk-core/tests/CMakeLists.txt +++ b/olp-cpp-sdk-core/tests/CMakeLists.txt @@ -46,6 +46,7 @@ set(OLP_CPP_SDK_CORE_TESTS_SOURCES ./geo/projection/IdentityProjectionTest.cpp ./geo/projection/SphereProjectionTest.cpp ./geo/projection/WebMercatorProjectionTest.cpp + ./geo/tiling/PathTilingTest.cpp ./geo/tiling/SubdivisionSchemeTest.cpp ./geo/tiling/SubTilesTest.cpp ./geo/tiling/TileKeyTest.cpp diff --git a/olp-cpp-sdk-core/tests/geo/tiling/PathTilingTest.cpp b/olp-cpp-sdk-core/tests/geo/tiling/PathTilingTest.cpp new file mode 100644 index 000000000..ef5b99211 --- /dev/null +++ b/olp-cpp-sdk-core/tests/geo/tiling/PathTilingTest.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2025 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#include +#include + +#include +#include + +namespace olp { +namespace geo { +namespace { +const auto kBerlin1 = GeoCoordinates::FromDegrees(52.514176, 13.339062); +const auto kBerlin2 = GeoCoordinates::FromDegrees(52.517029, 13.387142); +const auto kBerlin3 = GeoCoordinates::FromDegrees(52.490536, 13.397480); +} // namespace + +using DefaultScheme = HalfQuadTreeIdentityTilingScheme; + +TEST(AdjacentPairIterator, Iteration) { + std::vector coordinates{kBerlin1, kBerlin2, kBerlin3}; + + const auto start = coordinates.begin(); + const auto begin = MakeAdjacentPairIterator(*start, std::next(start)); + const auto end = MakeAdjacentPairIterator(coordinates.end()); + + std::vector> segments; + std::copy(begin, end, std::back_inserter(segments)); + + ASSERT_EQ(segments.size(), 2); + EXPECT_EQ(segments[0], std::make_tuple(kBerlin1, kBerlin2)); + EXPECT_EQ(segments[1], std::make_tuple(kBerlin2, kBerlin3)); +} + +TEST(TilingIterator, Iteration) { + std::vector coordinates{kBerlin1, kBerlin2, kBerlin3}; + + const auto begin = MakeTilingIterator(coordinates.begin(), 14); + const auto end = MakeTilingIterator(coordinates.end()); + + std::vector tiles; + std::copy(begin, end, std::back_inserter(tiles)); + + EXPECT_THAT(tiles, testing::ElementsAre(TileKey::FromQuadKey64(377893751), + TileKey::FromQuadKey64(377894441), + TileKey::FromQuadKey64(377894433))); +} + +TEST(TiledPathRange, Iteration) { + std::vector coordinates{kBerlin1, kBerlin2, kBerlin3}; + const auto range = MakeTiledPathRange( + coordinates.begin(), coordinates.end(), 16, 2); + + std::vector tiles; + std::transform(range.first, range.second, std::back_inserter(tiles), + [](const TileKey& key) { return key.ToQuadKey64(); }); + + constexpr uint64_t expected_tiles[] = { + 6046300013, 6046300015, 6046300101, 6046300024, 6046300026, 6046300112, + 6046300025, 6046300027, 6046300113, 6046300024, 6046300026, 6046300112, + 6046300025, 6046300027, 6046300113, 6046300028, 6046300030, 6046300116, + 6046300025, 6046300027, 6046300113, 6046300028, 6046300030, 6046300116, + 6046300029, 6046300031, 6046300117, 6046300028, 6046300030, 6046300116, + 6046300029, 6046300031, 6046300117, 6046310952, 6046310954, 6046311040, + 6046300029, 6046300031, 6046300117, 6046310952, 6046310954, 6046311040, + 6046310953, 6046310955, 6046311041, 6046310954, 6046311040, 6046311042, + 6046310955, 6046311041, 6046311043, 6046310958, 6046311044, 6046311046, + 6046310955, 6046311041, 6046311043, 6046310958, 6046311044, 6046311046, + 6046310959, 6046311045, 6046311047, 6046310958, 6046311044, 6046311046, + 6046310959, 6046311045, 6046311047, 6046310970, 6046311056, 6046311058, + 6046310959, 6046311045, 6046311047, 6046310970, 6046311056, 6046311058, + 6046310971, 6046311057, 6046311059, 6046310970, 6046311056, 6046311058, + 6046310971, 6046311057, 6046311059, 6046310974, 6046311060, 6046311062, + 6046310971, 6046310937, 6046310940, 6046310941, 6046310939, 6046310942, + 6046310943, 6046310961, 6046310964, 6046310965, 6046310939, 6046310942, + 6046310943, 6046310961, 6046310964, 6046310965, 6046310963, 6046310966, + 6046310967, 6046310961, 6046310964, 6046310965, 6046310963, 6046310966, + 6046310967, 6046310969, 6046310972, 6046310973, 6046310962, 6046310963, + 6046310966, 6046310968, 6046310969, 6046310972, 6046310970, 6046310971, + 6046310974, 6046310968, 6046310969, 6046310972, 6046310970, 6046310971, + 6046310974, 6046311056, 6046311057, 6046311060, 6046310970, 6046310971, + 6046310974, 6046311056, 6046311057, 6046311060, 6046311058, 6046311059, + 6046311062, 6046311056}; + + ASSERT_EQ(tiles.size(), sizeof(expected_tiles) / sizeof(uint64_t)); + + for (auto i = 0u; i < tiles.size(); ++i) { + ASSERT_EQ(tiles[i], expected_tiles[i]); + } +} + +} // namespace geo +} // namespace olp