Nav2 Navigation Stack - rolling  main
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 #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
24 namespace nav2_util
25 {
26 
27 PathSearchResult distance_from_path(
28  const nav_msgs::msg::Path & path,
29  const geometry_msgs::msg::Pose & robot_pose,
30  const size_t start_index,
31  const double search_window_length)
32 {
33  PathSearchResult result;
34  result.closest_segment_index = start_index;
35  result.distance = std::numeric_limits<double>::max();
36 
37  if (path.poses.empty()) {
38  return result;
39  }
40 
41  if (path.poses.size() == 1) {
42  result.distance = nav2_util::geometry_utils::euclidean_distance(
43  robot_pose, path.poses.front().pose);
44  result.closest_segment_index = 0;
45  return result;
46  }
47 
48  if (start_index >= path.poses.size()) {
49  throw std::runtime_error(
50  "Requested start index (" + std::to_string(start_index) +
51  ") is greater than or equal to path size (" + std::to_string(path.poses.size()) +
52  ").");
53  }
54 
55  double distance_traversed = 0.0;
56  for (size_t i = start_index; i < path.poses.size() - 1; ++i) {
57  if (distance_traversed > search_window_length) {
58  break;
59  }
60 
61  const double current_distance = geometry_utils::distance_to_path_segment(
62  robot_pose.position,
63  path.poses[i].pose,
64  path.poses[i + 1].pose);
65 
66  if (current_distance < result.distance) {
67  result.distance = current_distance;
68  result.closest_segment_index = i;
69  }
70 
71  distance_traversed += geometry_utils::euclidean_distance(
72  path.poses[i],
73  path.poses[i + 1]);
74  }
75 
76  const auto & segment_start = path.poses[result.closest_segment_index];
77  const auto & segment_end = path.poses[result.closest_segment_index + 1];
78 
79  // Obtain the signed direction of the cross track error
80  const double cross_product = geometry_utils::cross_product_2d(
81  robot_pose.position, segment_start.pose, segment_end.pose);
82  result.distance *= (cross_product >= 0.0 ? 1.0 : -1.0);
83 
84  return result;
85 }
86 
87 bool transformPathInTargetFrame(
88  const nav_msgs::msg::Path & input_path,
89  nav_msgs::msg::Path & transformed_path,
90  nav2::TransformBuffer & tf_buffer, const std::string target_frame,
91  const double transform_timeout)
92 {
93  static rclcpp::Logger logger = rclcpp::get_logger("transformPathInTargetFrame");
94 
95  if (input_path.header.frame_id == target_frame) {
96  transformed_path = input_path;
97  return true;
98  }
99 
100  transformed_path.header.frame_id = target_frame;
101  transformed_path.header.stamp = input_path.header.stamp;
102  transformed_path.poses.reserve(input_path.poses.size());
103 
104  if (input_path.poses.empty()) {
105  return true;
106  }
107 
108  geometry_msgs::msg::TransformStamped transform;
109  try {
110  transform = tf_buffer.lookupTransform(
111  target_frame, input_path.header.frame_id,
112  tf2_ros::fromMsg(input_path.header.stamp),
113  tf2::durationFromSec(transform_timeout));
114  } catch (const tf2::TransformException & ex) {
115  RCLCPP_ERROR(
116  logger,
117  "Failed to transform path from '%s' to '%s': %s",
118  input_path.header.frame_id.c_str(), target_frame.c_str(), ex.what());
119  return false;
120  }
121 
122  for (const auto & input_pose : input_path.poses) {
123  geometry_msgs::msg::PoseStamped source_pose, transformed_pose;
124  source_pose.header.frame_id = input_path.header.frame_id;
125  source_pose.header.stamp = input_path.header.stamp;
126  source_pose.pose = input_pose.pose;
127 
128  tf2::doTransform(source_pose, transformed_pose, transform);
129  transformed_pose.pose.position.z = 0.0;
130  transformed_path.poses.push_back(std::move(transformed_pose));
131  }
132 
133  return true;
134 }
135 
136 unsigned int findFirstPathConstraint(
137  nav_msgs::msg::Path & path,
138  bool enforce_path_inversion,
139  float rotation_threshold)
140 {
141  // At least 3 poses for a possible inversion
142  if (path.poses.size() < 3) {
143  return path.poses.size();
144  }
145 
146  const bool check_rotation = fabs(rotation_threshold) < 1e-6f ? false : true;
147  unsigned int rotation_idx = path.poses.size();
148  unsigned int inversion_idx = path.poses.size();
149  float prev_dx = 0.0f;
150  float prev_dy = 0.0f;
151 
152  // Iterating through the path to determine the position of the path inversion or rotation
153  for (unsigned int idx = 0; idx < path.poses.size() - 1; ++idx) {
154  float dx = path.poses[idx + 1].pose.position.x -
155  path.poses[idx].pose.position.x;
156  float dy = path.poses[idx + 1].pose.position.y -
157  path.poses[idx].pose.position.y;
158  float trans = hypot(dx, dy);
159 
160  // No smaller index can exist beyond this point, terminate early
161  if (rotation_idx <= idx + 1) {
162  break;
163  }
164 
165  // Check inversion
166  if (enforce_path_inversion && trans > 1e-4) {
167  if (idx >= 1) {
168  // Checking for the existence of cusp, in the path, using the dot product.
169  float dot_product = prev_dx * dx + prev_dy * dy;
170  if (dot_product < 0.0f) {
171  inversion_idx = idx + 1;
172  break;
173  }
174  }
175  prev_dx = dx;
176  prev_dy = dy;
177  }
178 
179  // Check in place rotation
180  if (check_rotation && trans < 1e-4 && rotation_idx == path.poses.size()) {
181  float accumulated_rotation = 0.0f;
182  unsigned int end_idx = idx;
183 
184  // Continue checking while translation remains small
185  // until accumulated rotation is larger than threshold
186  while (end_idx < path.poses.size() - 1) {
187  float current_yaw = tf2::getYaw(path.poses[end_idx].pose.orientation);
188  float next_yaw = tf2::getYaw(path.poses[end_idx + 1].pose.orientation);
189  accumulated_rotation += fabs(angles::shortest_angular_distance(current_yaw, next_yaw));
190  if (accumulated_rotation > rotation_threshold) {
191  rotation_idx = end_idx + 1;
192  break;
193  }
194  if (end_idx + 2 < path.poses.size()) {
195  float ndx = path.poses[end_idx + 2].pose.position.x -
196  path.poses[end_idx + 1].pose.position.x;
197  float ndy = path.poses[end_idx + 2].pose.position.y -
198  path.poses[end_idx + 1].pose.position.y;
199  // Stop if translation resumes
200  if (hypot(ndx, ndy) > 1e-4) {
201  break;
202  }
203  } else {
204  // We have reached the end of the path
205  break;
206  }
207  end_idx++;
208  }
209  }
210  }
211 
212  return std::min(rotation_idx, inversion_idx);
213 }
214 
215 unsigned int removePosesAfterFirstConstraint(
216  nav_msgs::msg::Path & path,
217  bool enforce_path_inversion,
218  float rotation_threshold)
219 {
220  nav_msgs::msg::Path cropped_path = path;
221  const unsigned int first_after_constraint = findFirstPathConstraint(cropped_path,
222  enforce_path_inversion, rotation_threshold);
223  if (first_after_constraint == path.poses.size()) {
224  return 0u;
225  }
226 
227  cropped_path.poses.erase(
228  cropped_path.poses.begin() + first_after_constraint, cropped_path.poses.end());
229  path = cropped_path;
230  return first_after_constraint;
231 }
232 
233 bool isPathUpdated(
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.size() != old_path.poses.size() &&
240  old_path.poses.back().pose.position == new_path.poses.back().pose.position;
241 }
242 
243 bool isGoalUpdated(
244  nav_msgs::msg::Path & new_path,
245  nav_msgs::msg::Path & old_path)
246 {
247  return old_path.poses.size() != 0 &&
248  new_path.poses.size() != 0 &&
249  new_path.poses.back().pose.position != old_path.poses.back().pose.position;
250 }
251 
252 } // namespace nav2_util