Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
path_utils.cpp
1 // Copyright (c) 2025 Berkan Tali
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "nav2_util/path_utils.hpp"
16 
17 #include <limits>
18 #include <cmath>
19 #include <stdexcept>
20 
21 #include "nav2_util/geometry_utils.hpp"
22 #include "nav2_ros_common/tf2_factories.hpp"
23 namespace nav2_util
24 {
25 
26 PathSearchResult distance_from_path(
27  const nav_msgs::msg::Path & path,
28  const geometry_msgs::msg::Pose & robot_pose,
29  const size_t start_index,
30  const double search_window_length)
31 {
32  PathSearchResult result;
33  result.closest_segment_index = start_index;
34  result.distance = std::numeric_limits<double>::max();
35 
36  if (path.poses.empty()) {
37  return result;
38  }
39 
40  if (path.poses.size() == 1) {
41  result.distance = nav2_util::geometry_utils::euclidean_distance(
42  robot_pose, path.poses.front().pose);
43  result.closest_segment_index = 0;
44  return result;
45  }
46 
47  if (start_index >= path.poses.size()) {
48  throw std::runtime_error(
49  "Requested start index (" + std::to_string(start_index) +
50  ") is greater than or equal to path size (" + std::to_string(path.poses.size()) +
51  ").");
52  }
53 
54  double distance_traversed = 0.0;
55  for (size_t i = start_index; i < path.poses.size() - 1; ++i) {
56  if (distance_traversed > search_window_length) {
57  break;
58  }
59 
60  const double current_distance = geometry_utils::distance_to_path_segment(
61  robot_pose.position,
62  path.poses[i].pose,
63  path.poses[i + 1].pose);
64 
65  if (current_distance < result.distance) {
66  result.distance = current_distance;
67  result.closest_segment_index = i;
68  }
69 
70  distance_traversed += geometry_utils::euclidean_distance(
71  path.poses[i],
72  path.poses[i + 1]);
73  }
74 
75  const auto & segment_start = path.poses[result.closest_segment_index];
76  const auto & segment_end = path.poses[result.closest_segment_index + 1];
77 
78  // Obtain the signed direction of the cross track error
79  const double cross_product = geometry_utils::cross_product_2d(
80  robot_pose.position, segment_start.pose, segment_end.pose);
81  result.distance *= (cross_product >= 0.0 ? 1.0 : -1.0);
82 
83  return result;
84 }
85 
86 bool transformPathInTargetFrame(
87  const nav_msgs::msg::Path & input_path,
88  nav_msgs::msg::Path & transformed_path,
89  nav2::TransformBuffer & tf_buffer, const std::string target_frame,
90  const double transform_timeout)
91 {
92  static rclcpp::Logger logger = rclcpp::get_logger("transformPathInTargetFrame");
93 
94  if (input_path.header.frame_id == target_frame) {
95  transformed_path = input_path;
96  return true;
97  }
98 
99  transformed_path.header.frame_id = target_frame;
100  transformed_path.header.stamp = input_path.header.stamp;
101  transformed_path.poses.reserve(input_path.poses.size());
102 
103  for (const auto & input_pose : input_path.poses) {
104  geometry_msgs::msg::PoseStamped source_pose, transformed_pose;
105  source_pose.header.frame_id = input_path.header.frame_id;
106  source_pose.header.stamp = input_path.header.stamp;
107  source_pose.pose = input_pose.pose;
108 
109  if (!nav2_util::transformPoseInTargetFrame(
110  source_pose, transformed_pose, tf_buffer, target_frame, transform_timeout))
111  {
112  RCLCPP_ERROR(
113  logger,
114  "Failed to transform path from '%s' to '%s'.",
115  input_path.header.frame_id.c_str(), target_frame.c_str());
116  return false;
117  }
118 
119  transformed_pose.pose.position.z = 0.0;
120  transformed_path.poses.push_back(std::move(transformed_pose));
121  }
122 
123  return true;
124 }
125 
126 unsigned int findFirstPathConstraint(
127  nav_msgs::msg::Path & path,
128  bool enforce_path_inversion,
129  float rotation_threshold)
130 {
131  // At least 3 poses for a possible inversion
132  if (path.poses.size() < 3) {
133  return path.poses.size();
134  }
135 
136  const bool check_rotation = fabs(rotation_threshold) < 1e-6f ? false : true;
137  unsigned int rotation_idx = path.poses.size();
138  unsigned int inversion_idx = path.poses.size();
139  float prev_dx = 0.0f;
140  float prev_dy = 0.0f;
141 
142  // Iterating through the path to determine the position of the path inversion or rotation
143  for (unsigned int idx = 0; idx < path.poses.size() - 1; ++idx) {
144  float dx = path.poses[idx + 1].pose.position.x -
145  path.poses[idx].pose.position.x;
146  float dy = path.poses[idx + 1].pose.position.y -
147  path.poses[idx].pose.position.y;
148  float trans = hypot(dx, dy);
149 
150  // No smaller index can exist beyond this point, terminate early
151  if (rotation_idx <= idx + 1) {
152  break;
153  }
154 
155  // Check inversion
156  if (enforce_path_inversion && trans > 1e-4) {
157  if (idx >= 1) {
158  // Checking for the existence of cusp, in the path, using the dot product.
159  float dot_product = prev_dx * dx + prev_dy * dy;
160  if (dot_product < 0.0f) {
161  inversion_idx = idx + 1;
162  break;
163  }
164  }
165  prev_dx = dx;
166  prev_dy = dy;
167  }
168 
169  // Check in place rotation
170  if (check_rotation && trans < 1e-4 && rotation_idx == path.poses.size()) {
171  float accumulated_rotation = 0.0f;
172  unsigned int end_idx = idx;
173 
174  // Continue checking while translation remains small
175  // until accumulated rotation is larger than threshold
176  while (end_idx < path.poses.size() - 1) {
177  float current_yaw = tf2::getYaw(path.poses[end_idx].pose.orientation);
178  float next_yaw = tf2::getYaw(path.poses[end_idx + 1].pose.orientation);
179  accumulated_rotation += fabs(angles::shortest_angular_distance(current_yaw, next_yaw));
180  if (accumulated_rotation > rotation_threshold) {
181  rotation_idx = end_idx + 1;
182  break;
183  }
184  if (end_idx + 2 < path.poses.size()) {
185  float ndx = path.poses[end_idx + 2].pose.position.x -
186  path.poses[end_idx + 1].pose.position.x;
187  float ndy = path.poses[end_idx + 2].pose.position.y -
188  path.poses[end_idx + 1].pose.position.y;
189  // Stop if translation resumes
190  if (hypot(ndx, ndy) > 1e-4) {
191  break;
192  }
193  } else {
194  // We have reached the end of the path
195  break;
196  }
197  end_idx++;
198  }
199  }
200  }
201 
202  return std::min(rotation_idx, inversion_idx);
203 }
204 
205 unsigned int removePosesAfterFirstConstraint(
206  nav_msgs::msg::Path & path,
207  bool enforce_path_inversion,
208  float rotation_threshold)
209 {
210  nav_msgs::msg::Path cropped_path = path;
211  const unsigned int first_after_constraint = findFirstPathConstraint(cropped_path,
212  enforce_path_inversion, rotation_threshold);
213  if (first_after_constraint == path.poses.size()) {
214  return 0u;
215  }
216 
217  cropped_path.poses.erase(
218  cropped_path.poses.begin() + first_after_constraint, cropped_path.poses.end());
219  path = cropped_path;
220  return first_after_constraint;
221 }
222 
223 bool isPathUpdated(
224  nav_msgs::msg::Path & new_path,
225  nav_msgs::msg::Path & old_path)
226 {
227  return old_path.poses.size() != 0 &&
228  new_path.poses.size() != 0 &&
229  new_path.poses.size() != old_path.poses.size() &&
230  old_path.poses.back().pose.position == new_path.poses.back().pose.position;
231 }
232 
233 bool isGoalUpdated(
234  nav_msgs::msg::Path & new_path,
235  nav_msgs::msg::Path & old_path)
236 {
237  return old_path.poses.size() != 0 &&
238  new_path.poses.size() != 0 &&
239  new_path.poses.back().pose.position != old_path.poses.back().pose.position;
240 }
241 
242 } // namespace nav2_util