From 868cb5dc770c14a96a2a976da13d9e0c093beb48 Mon Sep 17 00:00:00 2001 From: Christian Beiwinkel Date: Fri, 31 Jan 2025 21:48:19 +0100 Subject: [PATCH] Add headings and correlated lat/lon to verbose matrix output (#5072) --- CHANGELOG.md | 1 + docs/docs/api/matrix/api-reference.md | 2 +- proto/matrix.proto | 6 ++ src/thor/costmatrix.cc | 56 ++++++++++----- src/thor/timedistancebssmatrix.cc | 4 +- src/thor/timedistancematrix.cc | 3 +- src/tyr/matrix_serializer.cc | 25 +++++++ test/gurka/test_matrix.cc | 100 ++++++++++++++++++++++++++ test/matrix.cc | 10 ++- valhalla/thor/costmatrix.h | 8 +-- valhalla/thor/matrixalgorithm.h | 13 +++- 11 files changed, 201 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5232a6d3b3..2080ec6af5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * ADDED: Australian English language translations [#5057](https://github.com/valhalla/valhalla/pull/5057) * ADDED: Support `"access:conditional"` conditional restrictions like `"access:conditional"="no @ (Oct-May)"` [#5048](https://github.com/valhalla/valhalla/pull/5048) * CHANGED: Speed up pbf parsing by using libosmium [#5070](https://github.com/valhalla/valhalla/pull/5070) + * ADDED: headings and correlated ll's in verbose matrix output [#5072](https://github.com/valhalla/valhalla/pull/5072) ## Release Date: 2024-10-10 Valhalla 3.5.1 * **Removed** diff --git a/docs/docs/api/matrix/api-reference.md b/docs/docs/api/matrix/api-reference.md index f382e6981a..9e3b00e409 100644 --- a/docs/docs/api/matrix/api-reference.md +++ b/docs/docs/api/matrix/api-reference.md @@ -93,7 +93,7 @@ The following parameters are only present in `"verbose": true` mode: | :---- | :----------- | | `sources` | The sources passed to the request. | | `targets` | The targets passed to the request. | -| `sources_to_targets` | An array of time and distance between the sources and the targets.
The array is row-ordered, meaning the time and distance from the first location to all others forms the first row of the array, followed by the time and distance from the second source location to all target locations, etc.
The Object contained in the arrays contains the following fields: | +| `sources_to_targets` | An array of time and distance between the sources and the targets.
The array is row-ordered, meaning the time and distance from the first location to all others forms the first row of the array, followed by the time and distance from the second source location to all target locations, etc.
The Object contained in the arrays contains the following fields: | ### Concise mode (`"verbose": false`) diff --git a/proto/matrix.proto b/proto/matrix.proto index 54c47c0a1c..9510ed39c0 100644 --- a/proto/matrix.proto +++ b/proto/matrix.proto @@ -20,4 +20,10 @@ message Matrix { repeated string time_zone_offsets = 9; repeated string time_zone_names = 10; repeated bool second_pass = 11; + repeated float begin_heading = 12; + repeated float end_heading = 13; + repeated double begin_lat = 14; + repeated double begin_lon = 15; + repeated double end_lat = 16; + repeated double end_lon = 17; } diff --git a/src/thor/costmatrix.cc b/src/thor/costmatrix.cc index 602c90074d..476a2fbab4 100644 --- a/src/thor/costmatrix.cc +++ b/src/thor/costmatrix.cc @@ -118,7 +118,6 @@ bool CostMatrix::SourceToTarget(Api& request, const float max_matrix_distance) { request.mutable_matrix()->set_algorithm(Matrix::CostMatrix); bool invariant = request.options().date_time_type() == Options::invariant; - auto shape_format = request.options().shape_format(); // Set the mode and costing mode_ = mode; @@ -242,7 +241,7 @@ bool CostMatrix::SourceToTarget(Api& request, // resize/reserve all properties of Matrix on first pass only valhalla::Matrix& matrix = *request.mutable_matrix(); - reserve_pbf_arrays(matrix, best_connection_.size(), costing_->pass()); + reserve_pbf_arrays(matrix, best_connection_.size(), request.options().verbose(), costing_->pass()); // Form the matrix PBF output graph_tile_ptr tile; @@ -256,10 +255,8 @@ bool CostMatrix::SourceToTarget(Api& request, uint32_t target_idx = connection_idx % target_location_list.size(); uint32_t source_idx = connection_idx / target_location_list.size(); - // first recost and form the path, if desired (either time and/or geometry requested) - const auto shape = RecostFormPath(graphreader, best_connection, source_location_list[source_idx], - target_location_list[target_idx], source_idx, target_idx, - time_infos[source_idx], invariant, shape_format); + std::string shape = RecostFormPath(graphreader, best_connection, request, source_idx, target_idx, + connection_idx, time_infos[source_idx], invariant); float time = best_connection.cost.secs; if (time < kMaxCost && request.options().verbose()) { @@ -282,6 +279,7 @@ bool CostMatrix::SourceToTarget(Api& request, matrix.mutable_second_pass()->Set(connection_idx, true); connection_failed = true; } + matrix.mutable_from_indices()->Set(connection_idx, source_idx); matrix.mutable_to_indices()->Set(connection_idx, target_idx); matrix.mutable_distances()->Set(connection_idx, best_connection.distance); @@ -1190,16 +1188,15 @@ void CostMatrix::SetTargets(baldr::GraphReader& graphreader, // Form the path from the edfge labels and optionally return the shape std::string CostMatrix::RecostFormPath(GraphReader& graphreader, BestCandidate& connection, - const valhalla::Location& source, - const valhalla::Location& target, + Api& request, const uint32_t source_idx, const uint32_t target_idx, + const uint32_t connection_idx, const baldr::TimeInfo& time_info, - const bool invariant, - const ShapeFormat shape_format) { + const bool invariant) { // no need to look at source == target or missing connectivity - if ((!has_time_ && shape_format == no_shape) || connection.cost.secs == 0.f || - connection.distance == kMaxCost) { + if ((!has_time_ && request.options().shape_format() == no_shape && !request.options().verbose()) || + connection.cost.secs == 0.f || connection.distance == kMaxCost) { return ""; } @@ -1257,8 +1254,10 @@ std::string CostMatrix::RecostFormPath(GraphReader& graphreader, path_edges.emplace_back(std::move(opp_edge_id)); } - const auto& source_edge = find_correlated_edge(source, path_edges.front()); - const auto& target_edge = find_correlated_edge(target, path_edges.back()); + const auto& source_edge = + find_correlated_edge(request.options().sources(source_idx), path_edges.front()); + const auto& target_edge = + find_correlated_edge(request.options().targets(target_idx), path_edges.back()); float source_pct = static_cast(source_edge.percent_along()); float target_pct = static_cast(target_edge.percent_along()); @@ -1284,9 +1283,34 @@ std::string CostMatrix::RecostFormPath(GraphReader& graphreader, // update the existing best_connection cost connection.cost = new_cost; } + if (request.options().verbose()) { + + request.mutable_matrix()->mutable_begin_lat()->Set(connection_idx, source_edge.ll().lat()); + request.mutable_matrix()->mutable_begin_lon()->Set(connection_idx, source_edge.ll().lng()); + request.mutable_matrix()->mutable_end_lat()->Set(connection_idx, source_edge.ll().lat()); + request.mutable_matrix()->mutable_end_lon()->Set(connection_idx, source_edge.ll().lng()); + + // get begin/end heading using the path's begin/end edge shapes + const DirectedEdge* start_edge = + graphreader.directededge(static_cast(source_edge.graph_id()), tile); + std::vector shp = tile->edgeinfo(start_edge).shape(); + if (!start_edge->forward()) + std::reverse(shp.begin(), shp.end()); + request.mutable_matrix() + ->mutable_begin_heading() + ->Set(connection_idx, PointLL::HeadingAlongPolyline(shp, start_edge->length() * source_pct)); + const DirectedEdge* end_edge = + graphreader.directededge(static_cast(target_edge.graph_id()), tile); + shp = tile->edgeinfo(end_edge).shape(); + if (!end_edge->forward()) + std::reverse(shp.begin(), shp.end()); + request.mutable_matrix() + ->mutable_end_heading() + ->Set(connection_idx, PointLL::HeadingAlongPolyline(shp, end_edge->length() * target_pct)); + } // bail if no shape was requested - if (shape_format == no_shape) + if (request.options().shape_format() == no_shape) return ""; auto source_vertex = PointLL{source_edge.ll().lng(), source_edge.ll().lat()}; @@ -1325,7 +1349,7 @@ std::string CostMatrix::RecostFormPath(GraphReader& graphreader, } // encode to 6 precision for geojson as well, which the serializer expects - return encode(points, shape_format != polyline5 ? 1e6 : 1e5); + return encode(points, request.options().shape_format() != polyline5 ? 1e6 : 1e5); } template diff --git a/src/thor/timedistancebssmatrix.cc b/src/thor/timedistancebssmatrix.cc index 9f16424e90..f7a710ab3b 100644 --- a/src/thor/timedistancebssmatrix.cc +++ b/src/thor/timedistancebssmatrix.cc @@ -181,8 +181,10 @@ bool TimeDistanceBSSMatrix::ComputeMatrix(Api& request, // Initialize destinations once for all origins InitDestinations(graphreader, destinations); + // reserve the PBF vectors - reserve_pbf_arrays(*request.mutable_matrix(), origins.size() * destinations.size()); + reserve_pbf_arrays(*request.mutable_matrix(), origins.size() * destinations.size(), + request.options().verbose()); for (int origin_index = 0; origin_index < origins.size(); ++origin_index) { edgelabels_.reserve(max_reserved_labels_count_); diff --git a/src/thor/timedistancematrix.cc b/src/thor/timedistancematrix.cc index ec1376c1ca..9ecad23f51 100644 --- a/src/thor/timedistancematrix.cc +++ b/src/thor/timedistancematrix.cc @@ -205,7 +205,8 @@ bool TimeDistanceMatrix::ComputeMatrix(Api& request, // Initialize destinations once for all origins InitDestinations(graphreader, destinations); // reserve the PBF vectors - reserve_pbf_arrays(*request.mutable_matrix(), num_elements, costing_->pass()); + reserve_pbf_arrays(*request.mutable_matrix(), num_elements, request.options().verbose(), + costing_->pass()); for (int origin_index = 0; origin_index < origins.size(); ++origin_index) { // reserve some space for the next dijkstras (will be cleared at the end of the loop) diff --git a/src/tyr/matrix_serializer.cc b/src/tyr/matrix_serializer.cc index 73cffd2465..d8c6ed5ca6 100644 --- a/src/tyr/matrix_serializer.cc +++ b/src/tyr/matrix_serializer.cc @@ -139,6 +139,12 @@ json::ArrayPtr serialize_row(const valhalla::Matrix& matrix, const auto& date_time = matrix.date_times()[i]; const auto& time_zone_offset = matrix.time_zone_offsets()[i]; const auto& time_zone_name = matrix.time_zone_names()[i]; + const auto& begin_lat = matrix.begin_lat()[i]; + const auto& begin_lon = matrix.begin_lon()[i]; + const auto& end_lat = matrix.end_lat()[i]; + const auto& end_lon = matrix.end_lon()[i]; + const auto& begin_heading = matrix.begin_heading()[i]; + const auto& end_heading = matrix.end_heading()[i]; if (time != kMaxCost) { map = json::map({{"from_index", static_cast(source_index)}, {"to_index", static_cast(target_index + (i - start_td))}, @@ -156,6 +162,25 @@ json::ArrayPtr serialize_row(const valhalla::Matrix& matrix, map->emplace("time_zone_name", time_zone_name); } + if (begin_heading != kInvalidHeading) { + map->emplace("begin_heading", json::fixed_t{begin_heading, 0}); + } + + if (end_heading != kInvalidHeading) { + map->emplace("end_heading", json::fixed_t{end_heading, 0}); + } + if (begin_lat != INVALID_LL) { + map->emplace("begin_lat", json::fixed_t{begin_lat, 6}); + } + if (begin_lon != INVALID_LL) { + map->emplace("begin_lon", json::fixed_t{begin_lon, 6}); + } + if (end_lat != INVALID_LL) { + map->emplace("end_lat", json::fixed_t{end_lat, 6}); + } + if (end_lon != INVALID_LL) { + map->emplace("end_lon", json::fixed_t{end_lon, 6}); + } if (matrix.shapes().size() && shape_format != no_shape) { // TODO(nils): tdmatrices don't have "shape" support yet if (!matrix.shapes()[i].empty()) { diff --git a/test/gurka/test_matrix.cc b/test/gurka/test_matrix.cc index 5f418fd0cf..3170a9e065 100644 --- a/test/gurka/test_matrix.cc +++ b/test/gurka/test_matrix.cc @@ -864,6 +864,106 @@ TEST(StandAlone, HGVNoAccessPenalty) { } } +TEST(StandAlone, VerboseResponse) { + + const std::string ascii_map = R"( + A-1-------B----C----D----E--2-------F + | | + J----K + | | + | | + 3 4 + | | + L----M + )"; + + const gurka::ways ways = { + {"AB", {{"highway", "residential"}}}, {"BC", {{"highway", "residential"}}}, + {"CD", {{"highway", "residential"}}}, {"DE", {{"highway", "residential"}}}, + {"FE", {{"highway", "residential"}}}, {"CJ", {{"highway", "residential"}}}, + {"JK", {{"highway", "residential"}}}, {"JLMK", {{"highway", "residential"}}}, + {"KD", {{"highway", "residential"}}}, + }; + + const auto layout = gurka::detail::map_to_coordinates(ascii_map, 100); + gurka::map map = gurka::buildtiles(layout, ways, {}, {}, "test/data/matrix_verbose_response", + {{"service_limits.max_timedep_distance_matrix", "50000"}}); + rapidjson::Document res_doc; + std::string res; + { + auto api = gurka::do_action(valhalla::Options::sources_to_targets, map, {"1", "2", "3", "4"}, + {"1", "2", "3", "4"}, "auto", {{"/prioritize_bidirectional", "1"}}, + nullptr, &res); + + res_doc.Parse(res.c_str()); + + // sanity check + EXPECT_EQ(api.matrix().algorithm(), Matrix::CostMatrix); + + for (size_t i = 0; i < 4; ++i) { + for (size_t j = 0; j < 4; ++j) { + bool key_should_exist = true; + if (i == j) + key_should_exist = false; + EXPECT_EQ(res_doc["sources_to_targets"].GetArray()[i].GetArray()[j].GetObject().HasMember( + "begin_heading"), + key_should_exist); + EXPECT_EQ(res_doc["sources_to_targets"].GetArray()[i].GetArray()[j].GetObject().HasMember( + "end_heading"), + key_should_exist); + EXPECT_EQ(res_doc["sources_to_targets"].GetArray()[i].GetArray()[j].GetObject().HasMember( + "begin_lat"), + key_should_exist); + EXPECT_EQ(res_doc["sources_to_targets"].GetArray()[i].GetArray()[j].GetObject().HasMember( + "begin_lon"), + key_should_exist); + EXPECT_EQ(res_doc["sources_to_targets"].GetArray()[i].GetArray()[j].GetObject().HasMember( + "end_lat"), + key_should_exist); + EXPECT_EQ(res_doc["sources_to_targets"].GetArray()[i].GetArray()[j].GetObject().HasMember( + "end_lon"), + key_should_exist); + } + } + EXPECT_EQ(res_doc["sources_to_targets"] + .GetArray()[0] + .GetArray()[1] + .GetObject()["begin_heading"] + .GetDouble(), + 90); + EXPECT_EQ(res_doc["sources_to_targets"] + .GetArray()[0] + .GetArray()[1] + .GetObject()["end_heading"] + .GetDouble(), + 90); + EXPECT_EQ(res_doc["sources_to_targets"] + .GetArray()[1] + .GetArray()[0] + .GetObject()["begin_heading"] + .GetDouble(), + 270); + EXPECT_EQ(res_doc["sources_to_targets"] + .GetArray()[1] + .GetArray()[0] + .GetObject()["end_heading"] + .GetDouble(), + 270); + EXPECT_EQ(res_doc["sources_to_targets"] + .GetArray()[0] + .GetArray()[2] + .GetObject()["begin_heading"] + .GetDouble(), + 90); + EXPECT_EQ(res_doc["sources_to_targets"] + .GetArray()[0] + .GetArray()[2] + .GetObject()["end_heading"] + .GetDouble(), + 180); + } +} + /************************************************************************ */ std::string encode_shape(const std::vector& nodes, valhalla::gurka::nodelayout& layout) { diff --git a/test/matrix.cc b/test/matrix.cc index 8bfb50c1c5..96c3f79cbe 100644 --- a/test/matrix.cc +++ b/test/matrix.cc @@ -486,13 +486,19 @@ TEST(Matrix, default_matrix) { EXPECT_TRUE(json.HasMember("sources_to_targets")); - // contains 4 keys i.e, "distance", "time", "to_index" and "from_index" - EXPECT_EQ(json["sources_to_targets"].GetArray()[0][0].MemberCount(), 4); + // contains 10 keys + EXPECT_EQ(json["sources_to_targets"].GetArray()[0][0].MemberCount(), 10); EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("distance")); EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("time")); EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("to_index")); EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("from_index")); + EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("begin_heading")); + EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("end_heading")); + EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("begin_lat")); + EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("begin_lon")); + EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("end_lat")); + EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].HasMember("end_lon")); EXPECT_TRUE(json["sources_to_targets"].GetArray()[0][0].IsObject()); diff --git a/valhalla/thor/costmatrix.h b/valhalla/thor/costmatrix.h index 4e855349c1..4ec70edbaf 100644 --- a/valhalla/thor/costmatrix.h +++ b/valhalla/thor/costmatrix.h @@ -305,14 +305,12 @@ class CostMatrix : public MatrixAlgorithm { */ std::string RecostFormPath(baldr::GraphReader& graphreader, BestCandidate& connection, - const valhalla::Location& source, - const valhalla::Location& target, + Api& request, const uint32_t source_idx, const uint32_t target_idx, + const uint32_t connection_idx, const baldr::TimeInfo& time_info, - const bool invariant, - const ShapeFormat shape_format); - + const bool invariant); /** * Sets the date_time on the origin locations. * diff --git a/valhalla/thor/matrixalgorithm.h b/valhalla/thor/matrixalgorithm.h index 3e0fb34a91..0719a87cd3 100644 --- a/valhalla/thor/matrixalgorithm.h +++ b/valhalla/thor/matrixalgorithm.h @@ -17,6 +17,7 @@ namespace thor { // Default for time distance matrix is to find all locations constexpr uint32_t kAllLocations = std::numeric_limits::max(); +constexpr float kInvalidHeading = std::numeric_limits::max(); constexpr float kMaxCost = 99999999.9999f; /** @@ -151,7 +152,8 @@ class MatrixAlgorithm { bool clear_reserved_memory_; // on first pass, resizes all PBF sequences and defaults to 0 or "" - inline static void reserve_pbf_arrays(valhalla::Matrix& matrix, size_t size, uint32_t pass = 0) { + inline static void + reserve_pbf_arrays(valhalla::Matrix& matrix, size_t size, bool verbose, uint32_t pass = 0) { if (pass == 0) { matrix.mutable_from_indices()->Resize(size, 0U); matrix.mutable_to_indices()->Resize(size, 0U); @@ -173,6 +175,15 @@ class MatrixAlgorithm { auto* shape = matrix.mutable_shapes()->Add(); *shape = ""; } + if (verbose) { + // fill with sentinel values meaning "no data" + matrix.mutable_begin_heading()->Resize(size, kInvalidHeading); + matrix.mutable_end_heading()->Resize(size, kInvalidHeading); + matrix.mutable_begin_lat()->Resize(size, INVALID_LL); + matrix.mutable_begin_lon()->Resize(size, INVALID_LL); + matrix.mutable_end_lat()->Resize(size, INVALID_LL); + matrix.mutable_end_lon()->Resize(size, INVALID_LL); + } } } };