Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
feasible_path_handler.cpp
1 // Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov
2 // Copyright (c) 2023 Dexory
3 // Copyright (c) 2023 Open Navigation LLC
4 //
5 // Licensed under the Apache License, Version 2.0 (the "License");
6 // you may not use this file except in compliance with the License.
7 // You may obtain a copy of the License at
8 //
9 // http://www.apache.org/licenses/LICENSE-2.0
10 //
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
16 
17 #include <memory>
18 #include "angles/angles.h"
19 #include "pluginlib/class_list_macros.hpp"
20 #include "nav2_controller/plugins/feasible_path_handler.hpp"
21 #include "nav2_util/path_utils.hpp"
22 #include "nav2_util/geometry_utils.hpp"
23 #include "nav2_core/controller_exceptions.hpp"
24 #include "nav2_ros_common/tf2_factories.hpp"
25 
26 
27 using rcl_interfaces::msg::ParameterType;
28 using std::placeholders::_1;
29 
30 namespace nav2_controller
31 {
32 using nav2_util::geometry_utils::euclidean_distance;
33 
35 {
36  auto node = node_.lock();
37  if (post_set_params_handler_ && node) {
38  node->remove_post_set_parameters_callback(post_set_params_handler_.get());
39  }
40  post_set_params_handler_.reset();
41  if (on_set_params_handler_ && node) {
42  node->remove_on_set_parameters_callback(on_set_params_handler_.get());
43  }
44  on_set_params_handler_.reset();
45 }
46 
48  const nav2::LifecycleNode::WeakPtr & parent,
49  const rclcpp::Logger & logger,
50  const std::string & plugin_name,
51  const std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros,
52  nav2::TransformBuffer::SharedPtr tf)
53 {
54  logger_ = logger;
55  plugin_name_ = plugin_name;
56  node_ = parent;
57  auto node = node_.lock();
58  costmap_ros_ = costmap_ros;
59  tf_ = tf;
60  transform_tolerance_ = costmap_ros_->getTransformTolerance();
61  reject_unit_path_ = node->declare_or_get_parameter(
62  plugin_name + ".reject_unit_path", false);
63  max_robot_pose_search_dist_ = node->declare_or_get_parameter(
64  plugin_name + ".max_robot_pose_search_dist", getCostmapMaxExtent());
65  prune_distance_ = node->declare_or_get_parameter(
66  plugin_name + ".prune_distance", 2.0);
67  enforce_path_inversion_ = node->declare_or_get_parameter(
68  plugin_name + ".enforce_path_inversion", false);
69  enforce_path_rotation_ = node->declare_or_get_parameter(
70  plugin_name + ".enforce_path_rotation", false);
71  inversion_xy_tolerance_ = node->declare_or_get_parameter(
72  plugin_name + ".inversion_xy_tolerance", 0.2);
73  inversion_yaw_tolerance_ = node->declare_or_get_parameter(
74  plugin_name + ".inversion_yaw_tolerance", 0.4);
75  minimum_rotation_angle_ = node->declare_or_get_parameter(
76  plugin_name + ".minimum_rotation_angle", 0.785);
77  if (max_robot_pose_search_dist_ < 0.0) {
78  RCLCPP_WARN(
79  logger_, "Max robot search distance is negative, setting to max to search"
80  " every point on path for the closest value.");
81  max_robot_pose_search_dist_ = std::numeric_limits<double>::max();
82  }
83  constraint_locale_ = 0u;
84  if (!enforce_path_rotation_) {
85  minimum_rotation_angle_ = 0.0f;
86  }
87 
88  // Add callback for dynamic parameters
89  post_set_params_handler_ = node->add_post_set_parameters_callback(
90  std::bind(
92  this, std::placeholders::_1));
93  on_set_params_handler_ = node->add_on_set_parameters_callback(
94  std::bind(
96  this, std::placeholders::_1));
97 }
98 
100 {
101  const double max_costmap_dim_meters = std::max(
102  costmap_ros_->getCostmap()->getSizeInMetersX(),
103  costmap_ros_->getCostmap()->getSizeInMetersY());
104  return max_costmap_dim_meters / 2.0;
105 }
106 
107 void FeasiblePathHandler::prunePlan(nav_msgs::msg::Path & plan, const nav2_core::PathIterator end)
108 {
109  plan.poses.erase(plan.poses.begin(), end);
110 }
111 
113  const geometry_msgs::msg::PoseStamped & robot_pose)
114 {
115  // Keep full path if we are within tolerance of the inversion pose
116  const auto last_pose = global_plan_up_to_constraint_.poses.back();
117  float distance = hypotf(
118  robot_pose.pose.position.x - last_pose.pose.position.x,
119  robot_pose.pose.position.y - last_pose.pose.position.y);
120 
121  float angle_distance = angles::shortest_angular_distance(
122  tf2::getYaw(robot_pose.pose.orientation),
123  tf2::getYaw(last_pose.pose.orientation));
124 
125  return distance <= inversion_xy_tolerance_ &&
126  fabs(angle_distance) <= inversion_yaw_tolerance_;
127 }
128 
129 void FeasiblePathHandler::setPlan(const nav_msgs::msg::Path & path)
130 {
131  std::lock_guard<std::mutex> lock_reinit(mutex_);
132  global_plan_ = path;
133  global_plan_up_to_constraint_ = global_plan_;
134  if (enforce_path_inversion_ || enforce_path_rotation_) {
135  constraint_locale_ = nav2_util::removePosesAfterFirstConstraint(global_plan_up_to_constraint_,
136  enforce_path_inversion_, minimum_rotation_angle_);
137  }
138 }
139 
140 geometry_msgs::msg::PoseStamped FeasiblePathHandler::transformToGlobalPlanFrame(
141  const geometry_msgs::msg::PoseStamped & pose)
142 {
143  if (global_plan_up_to_constraint_.poses.empty()) {
144  throw nav2_core::InvalidPath("Received plan with zero length");
145  }
146 
147  if (reject_unit_path_ && global_plan_up_to_constraint_.poses.size() == 1) {
148  throw nav2_core::InvalidPath("Received plan with length of one");
149  }
150 
151  // let's get the pose of the robot in the frame of the plan
152  geometry_msgs::msg::PoseStamped robot_pose;
153  if (!nav2_util::transformPoseInTargetFrame(pose, robot_pose, *tf_,
154  global_plan_up_to_constraint_.header.frame_id,
155  transform_tolerance_))
156  {
157  throw nav2_core::ControllerTFError("Unable to transform robot pose into global plan's frame");
158  }
159 
160  return robot_pose;
161 }
162 
164  const geometry_msgs::msg::PoseStamped & pose)
165 {
166  std::lock_guard<std::mutex> lock_reinit(mutex_);
167  global_pose_ = transformToGlobalPlanFrame(pose);
168 
169  // Limit the search for the closest pose up to max_robot_pose_search_dist on the path
170  auto closest_pose_upper_bound =
171  nav2_util::geometry_utils::first_after_integrated_distance(
172  global_plan_up_to_constraint_.poses.begin(), global_plan_up_to_constraint_.poses.end(),
173  max_robot_pose_search_dist_);
174 
175  // First find the closest pose on the path to the robot
176  // bounded by when the path turns around (if it does) so we don't get a pose from a later
177  // portion of the path
178  auto closest_point =
179  nav2_util::geometry_utils::min_by(
180  global_plan_up_to_constraint_.poses.begin(), closest_pose_upper_bound,
181  [this](const geometry_msgs::msg::PoseStamped & ps) {
182  return euclidean_distance(global_pose_, ps);
183  });
184 
185  // Do not prune the global plan below 2 points, so there are enough points to interpolate the
186  // end-of-path direction.
187  if (global_plan_up_to_constraint_.poses.size() > 1 &&
188  closest_point == std::prev(global_plan_up_to_constraint_.poses.end()))
189  {
190  closest_point = std::prev(closest_point);
191  }
192 
193  auto pruned_plan_end =
194  nav2_util::geometry_utils::first_after_integrated_distance(
195  closest_point, global_plan_up_to_constraint_.poses.end(), prune_distance_);
196 
197  return {closest_point, pruned_plan_end};
198 }
199 
200 
202  const nav2_core::PathIterator & closest_point,
203  const nav2_core::PathIterator & pruned_plan_end)
204 {
205  std::lock_guard<std::mutex> lock_reinit(mutex_);
206  nav_msgs::msg::Path transformed_plan;
207  transformed_plan.header.frame_id = costmap_ros_->getGlobalFrameID();
208  transformed_plan.header.stamp = global_pose_.header.stamp;
209  unsigned int mx, my;
210  geometry_msgs::msg::TransformStamped plan_to_costmap;
211  try {
212  // Uses timestamp which has already been validated for staleness
213  plan_to_costmap = tf_->lookupTransform(
214  costmap_ros_->getGlobalFrameID(),
215  global_plan_.header.frame_id,
216  global_pose_.header.stamp,
217  tf2::durationFromSec(transform_tolerance_));
218  } catch (const tf2::TransformException & ex) {
220  std::string("Unable to transform global plan into costmap frame: ") +
221  ex.what());
222  }
223 
224  // Find the furthest relevant pose on the path to consider within costmap
225  // bounds
226  // Transforming it to the costmap frame in the same loop
227  std::lock_guard<nav2_costmap_2d::Costmap2D::mutex_t> costmap_lock(
228  *(costmap_ros_->getCostmap()->getMutex()));
229  for (auto global_plan_pose = closest_point; global_plan_pose != pruned_plan_end;
230  ++global_plan_pose)
231  {
232  geometry_msgs::msg::PoseStamped plan_pose = *global_plan_pose;
233  plan_pose.header = global_pose_.header;
234  geometry_msgs::msg::PoseStamped costmap_plan_pose;
235  tf2::doTransform(plan_pose, costmap_plan_pose, plan_to_costmap);
236  if (!costmap_ros_->getCostmap()->worldToMap(
237  costmap_plan_pose.pose.position.x,
238  costmap_plan_pose.pose.position.y, mx, my))
239  {
240  break;
241  }
242 
243  transformed_plan.poses.push_back(costmap_plan_pose);
244  }
245 
246  // Remove the portion of the global plan that we've already passed so we don't
247  // process it on the next iteration (this is called path pruning)
248  prunePlan(global_plan_up_to_constraint_, closest_point);
249 
250  if ((enforce_path_inversion_ || enforce_path_rotation_) && constraint_locale_ != 0u) {
251  if (isWithinInversionTolerances(global_pose_)) {
252  prunePlan(global_plan_, global_plan_.poses.begin() + constraint_locale_);
253  global_plan_up_to_constraint_ = global_plan_;
254  constraint_locale_ = nav2_util::removePosesAfterFirstConstraint(global_plan_up_to_constraint_,
255  enforce_path_inversion_, minimum_rotation_angle_);
256  }
257  }
258 
259  if (transformed_plan.poses.empty()) {
260  throw nav2_core::InvalidPath("Resulting plan has 0 poses in it.");
261  }
262 
263  return transformed_plan;
264 }
265 
266 geometry_msgs::msg::PoseStamped FeasiblePathHandler::getTransformedGoal(
267  const builtin_interfaces::msg::Time & stamp)
268 {
269  auto goal = global_plan_.poses.back();
270  goal.header.frame_id = global_plan_.header.frame_id;
271  goal.header.stamp = stamp;
272  if (goal.header.frame_id.empty()) {
273  throw nav2_core::ControllerTFError("Goal pose has an empty frame_id");
274  }
275  geometry_msgs::msg::PoseStamped transformed_goal;
276  if (!nav2_util::transformPoseInTargetFrame(goal, transformed_goal, *costmap_ros_->getTfBuffer(),
277  costmap_ros_->getGlobalFrameID(), transform_tolerance_))
278  {
279  throw nav2_core::ControllerTFError("Unable to transform goal pose into costmap frame");
280  }
281  return transformed_goal;
282 }
283 
284 rcl_interfaces::msg::SetParametersResult
286  const std::vector<rclcpp::Parameter> & parameters)
287 {
288  rcl_interfaces::msg::SetParametersResult result;
289  result.successful = true;
290  for (const auto & parameter : parameters) {
291  const auto & param_type = parameter.get_type();
292  const auto & param_name = parameter.get_name();
293  if (param_name.find(plugin_name_ + ".") != 0) {
294  continue;
295  }
296  if (param_type == ParameterType::PARAMETER_DOUBLE) {
297  if (parameter.as_double() < 0.0) {
298  RCLCPP_WARN(
299  logger_, "The value of parameter '%s' is incorrectly set to %f, "
300  "it should be >=0. Ignoring parameter update.",
301  param_name.c_str(), parameter.as_double());
302  result.successful = false;
303  }
304  }
305  }
306  return result;
307 }
308 
309 void
311  const std::vector<rclcpp::Parameter> & parameters)
312 {
313  std::lock_guard<std::mutex> lock_reinit(mutex_);
314  rcl_interfaces::msg::SetParametersResult result;
315  for (const auto & parameter : parameters) {
316  const auto & param_type = parameter.get_type();
317  const auto & param_name = parameter.get_name();
318  if (param_name.find(plugin_name_ + ".") != 0) {
319  continue;
320  }
321  if (param_type == ParameterType::PARAMETER_DOUBLE) {
322  if (param_name == plugin_name_ + ".max_robot_pose_search_dist") {
323  max_robot_pose_search_dist_ = parameter.as_double();
324  } else if (param_name == plugin_name_ + ".inversion_xy_tolerance") {
325  inversion_xy_tolerance_ = parameter.as_double();
326  } else if (param_name == plugin_name_ + ".inversion_yaw_tolerance") {
327  inversion_yaw_tolerance_ = parameter.as_double();
328  } else if (param_name == plugin_name_ + ".prune_distance") {
329  prune_distance_ = parameter.as_double();
330  } else if (param_name == plugin_name_ + ".minimum_rotation_angle") {
331  minimum_rotation_angle_ = parameter.as_double();
332  }
333  } else if (param_type == ParameterType::PARAMETER_BOOL) {
334  if (param_name == plugin_name_ + ".enforce_path_inversion") {
335  enforce_path_inversion_ = parameter.as_bool();
336  } else if (param_name == plugin_name_ + ".enforce_path_rotation") {
337  enforce_path_rotation_ = parameter.as_bool();
338  if (!enforce_path_rotation_) {
339  minimum_rotation_angle_ = 0.0f;
340  }
341  }
342  }
343  }
344 }
345 
346 } // namespace nav2_controller
347 
This plugin manages the global plan by clipping it to the local segment, typically bounded by the loc...
~FeasiblePathHandler()
Destroy the Feasible Path Handler object.
void prunePlan(nav_msgs::msg::Path &plan, const nav2_core::PathIterator end)
Prune a path to only interesting portions.
nav_msgs::msg::Path transformLocalPlan(const nav2_core::PathIterator &closest_point, const nav2_core::PathIterator &pruned_plan_end) override
Transforms a predefined segment of the global plan into the costmap global frame.
bool isWithinInversionTolerances(const geometry_msgs::msg::PoseStamped &robot_pose)
Check if the robot pose is within the set inversion tolerances.
geometry_msgs::msg::PoseStamped transformToGlobalPlanFrame(const geometry_msgs::msg::PoseStamped &pose)
Transform a pose to the global reference frame.
void setPlan(const nav_msgs::msg::Path &path) override
Set new reference plan.
void updateParametersCallback(const std::vector< rclcpp::Parameter > &parameters)
Apply parameter updates after validation This callback is executed when parameters have been successf...
geometry_msgs::msg::PoseStamped getTransformedGoal(const builtin_interfaces::msg::Time &stamp) override
Get the global goal pose transformed to the costmap global frame.
rcl_interfaces::msg::SetParametersResult validateParameterUpdatesCallback(const std::vector< rclcpp::Parameter > &parameters)
Validate incoming parameter updates before applying them. This callback is triggered when one or more...
nav2_core::PathSegment findPlanSegment(const geometry_msgs::msg::PoseStamped &pose) override
Determines the portion of the global plan to be used for local control. This function locates the sta...
void initialize(const nav2::LifecycleNode::WeakPtr &parent, const rclcpp::Logger &logger, const std::string &plugin_name, const std::shared_ptr< nav2_costmap_2d::Costmap2DROS > costmap_ros, nav2::TransformBuffer::SharedPtr tf) override
Initialize parameters.
Function-object for handling the path from Planner Server.