Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
is_path_valid_service.hpp
1 // Copyright (c) 2025 Dexory
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 #ifndef NAV2_PLANNER__IS_PATH_VALID_SERVICE_HPP_
16 #define NAV2_PLANNER__IS_PATH_VALID_SERVICE_HPP_
17 
18 #include <memory>
19 #include <limits>
20 #include <string>
21 
22 #include "rclcpp/rclcpp.hpp"
23 #include "rclcpp_lifecycle/lifecycle_node.hpp"
24 #include "nav2_msgs/srv/is_path_valid.hpp"
25 #include "nav2_costmap_2d/costmap_2d_ros.hpp"
26 #include "nav2_costmap_2d/footprint_collision_checker.hpp"
27 #include "nav2_costmap_2d/cost_values.hpp"
28 #include "nav2_costmap_2d/costmap_layer.hpp"
29 #include "nav2_util/geometry_utils.hpp"
30 #include "nav2_ros_common/service_server.hpp"
31 #include "tf2/utils.hpp"
32 
33 namespace nav2_planner
34 {
35 
41 {
42 public:
50  nav2::LifecycleNode::WeakPtr node,
51  std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros,
52  const rclcpp::Duration & costmap_update_timeout)
53  : node_(node), costmap_ros_(costmap_ros), logger_(rclcpp::get_logger("is_path_valid_service")),
54  costmap_update_timeout_(costmap_update_timeout)
55  {
56  }
57 
61  void initialize()
62  {
63  auto node = node_.lock();
64  if (!node) {
65  throw std::runtime_error("Failed to lock node in initialize");
66  }
67 
68  costmap_ = costmap_ros_->getCostmap();
69 
70  service_ = node->create_service<nav2_msgs::srv::IsPathValid>(
71  "is_path_valid",
72  std::bind(
73  &IsPathValidService::callback, this,
74  std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
75  }
76 
80  void reset()
81  {
82  service_.reset();
83  costmap_ = nullptr;
84  }
85 
86 private:
92  nav2_costmap_2d::Costmap2D * getCostmapToCheck(const std::string & layer_name)
93  {
94  if (layer_name.empty()) {
95  return costmap_;
96  }
97 
98  auto layers = costmap_ros_->getLayeredCostmap()->getPlugins();
99  for (auto & layer : *layers) {
100  if (layer->getName() == layer_name) {
101  auto * costmap_layer = dynamic_cast<nav2_costmap_2d::CostmapLayer *>(layer.get());
102  if (costmap_layer != nullptr) {
103  return static_cast<nav2_costmap_2d::Costmap2D *>(costmap_layer);
104  }
105  }
106  }
107 
108  RCLCPP_ERROR(
109  logger_, "Requested layer '%s' not found or does not provide a costmap.",
110  layer_name.c_str());
111  return nullptr;
112  }
113 
121  bool getFootprintToUse(
122  const std::string & footprint_string,
123  nav2_costmap_2d::Footprint & footprint,
124  bool & use_radius)
125  {
126  use_radius = costmap_ros_->getUseRadius();
127 
128  if (!footprint_string.empty()) {
129  if (!nav2_costmap_2d::makeFootprintFromString(footprint_string, footprint)) {
130  RCLCPP_ERROR(
131  logger_, "Invalid footprint string '%s'. Cannot validate path.",
132  footprint_string.c_str());
133  return false;
134  }
135  use_radius = false;
136  } else if (!use_radius) {
137  footprint = costmap_ros_->getRobotFootprint();
138  }
139 
140  return true;
141  }
142 
149  unsigned int findClosestPointIndex(
150  const geometry_msgs::msg::PoseStamped & current_pose,
151  const nav_msgs::msg::Path & path)
152  {
153  unsigned int closest_point_index = 0;
154  float closest_distance = std::numeric_limits<float>::max();
155  const auto & current_point = current_pose.pose.position;
156 
157  for (unsigned int i = 0; i < path.poses.size(); ++i) {
158  const auto & path_point = path.poses[i].pose.position;
159  float distance = nav2_util::geometry_utils::euclidean_distance(current_point, path_point);
160 
161  if (distance < closest_distance) {
162  closest_point_index = i;
163  closest_distance = distance;
164  }
165  }
166 
167  return closest_point_index;
168  }
169 
173  void callback(
174  const std::shared_ptr<rmw_request_id_t>/*request_header*/,
175  const std::shared_ptr<nav2_msgs::srv::IsPathValid::Request> request,
176  std::shared_ptr<nav2_msgs::srv::IsPathValid::Response> response)
177  {
178  response->success = true;
179  response->is_valid = true;
180 
181  if (request->path.poses.empty()) {
182  RCLCPP_ERROR(logger_, "Received empty path. Cannot validate path.");
183  response->success = false;
184  response->is_valid = false;
185  return;
186  }
187 
188  // Wait for costmap to become current (skip if timeout is zero or negative)
189  if (costmap_update_timeout_ > rclcpp::Duration(0, 0)) {
190  try {
191  costmap_ros_->waitUntilCurrent(costmap_update_timeout_);
192  } catch (const std::exception & ex) {
193  RCLCPP_ERROR(logger_, "Failed to wait for costmap: %s", ex.what());
194  response->success = false;
195  response->is_valid = false;
196  return;
197  }
198  }
199 
200  geometry_msgs::msg::PoseStamped current_pose;
201  if (!costmap_ros_->getRobotPose(current_pose)) {
202  RCLCPP_ERROR(logger_, "Failed to get robot pose. Cannot validate path.");
203  response->success = false;
204  response->is_valid = false;
205  return;
206  }
207 
213  unsigned int closest_point_index = findClosestPointIndex(current_pose, request->path);
214 
215  // Determine which costmap to use based on layer_name parameter
216  nav2_costmap_2d::Costmap2D * costmap_to_check = getCostmapToCheck(request->layer_name);
217 
218  if (!costmap_to_check) {
219  response->success = false;
220  response->is_valid = false;
221  return;
222  }
223 
224  std::unique_lock<nav2_costmap_2d::Costmap2D::mutex_t> lock(
225  *(costmap_to_check->getMutex()));
226  unsigned int mx = 0;
227  unsigned int my = 0;
228 
229  // Determine footprint to use
230  nav2_costmap_2d::Footprint footprint;
231  bool use_radius;
232 
233  if (!getFootprintToUse(request->footprint, footprint, use_radius)) {
234  response->success = false;
235  response->is_valid = false;
236  return;
237  }
238 
239  // Create collision checker for this request if needed
240  std::unique_ptr<nav2_costmap_2d::FootprintCollisionChecker<nav2_costmap_2d::Costmap2D *>>
241  collision_checker;
242  if (!use_radius) {
243  collision_checker =
244  std::make_unique<nav2_costmap_2d::FootprintCollisionChecker<nav2_costmap_2d::Costmap2D *>>(
245  costmap_to_check);
246  }
247 
248  // Determine the end index for validation based on lookahead distance
249  const auto & poses = request->path.poses;
250  unsigned int end_index = poses.size();
251  if (request->max_lookahead_distance > 0.0) {
252  auto end_it = nav2_util::geometry_utils::first_after_integrated_distance(
253  poses.begin() + closest_point_index, poses.end(),
254  request->max_lookahead_distance);
255  end_index = std::distance(poses.begin(), end_it);
256  }
257 
258  unsigned int cost = nav2_costmap_2d::FREE_SPACE;
259  for (unsigned int i = closest_point_index; i < end_index; ++i) {
260  auto & position = request->path.poses[i].pose.position;
261  if (use_radius) {
262  if (costmap_to_check->worldToMap(position.x, position.y, mx, my)) {
263  cost = costmap_to_check->getCost(mx, my);
264  } else {
265  cost = nav2_costmap_2d::LETHAL_OBSTACLE;
266  }
267  } else {
268  auto theta = tf2::getYaw(request->path.poses[i].pose.orientation);
269  cost = static_cast<unsigned int>(collision_checker->footprintCostAtPose(
270  position.x, position.y, theta, footprint));
271  }
272 
273  if (cost == nav2_costmap_2d::NO_INFORMATION && request->consider_unknown_as_obstacle) {
274  cost = nav2_costmap_2d::LETHAL_OBSTACLE;
275  } else if (cost == nav2_costmap_2d::NO_INFORMATION) {
276  cost = nav2_costmap_2d::FREE_SPACE;
277  }
278 
279  if (use_radius &&
280  (cost >= request->max_cost || cost == nav2_costmap_2d::LETHAL_OBSTACLE ||
281  cost == nav2_costmap_2d::INSCRIBED_INFLATED_OBSTACLE))
282  {
283  response->is_valid = false;
284  response->invalid_pose_indices.push_back(i);
285  if (request->stop_at_first_collision) {
286  break;
287  }
288  } else if (cost == nav2_costmap_2d::LETHAL_OBSTACLE || cost >= request->max_cost) {
289  response->is_valid = false;
290  response->invalid_pose_indices.push_back(i);
291  if (request->stop_at_first_collision) {
292  break;
293  }
294  }
295  }
296  }
297 
298  nav2::LifecycleNode::WeakPtr node_;
299  std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros_;
300  nav2_costmap_2d::Costmap2D * costmap_;
301  rclcpp::Logger logger_;
302  rclcpp::Duration costmap_update_timeout_;
303  nav2::ServiceServer<nav2_msgs::srv::IsPathValid>::SharedPtr service_;
304 };
305 
306 } // namespace nav2_planner
307 
308 #endif // NAV2_PLANNER__IS_PATH_VALID_SERVICE_HPP_
A 2D costmap provides a mapping between points in the world and their associated "costs".
Definition: costmap_2d.hpp:69
unsigned char getCost(unsigned int mx, unsigned int my) const
Get the cost of a cell in the costmap.
Definition: costmap_2d.cpp:265
bool worldToMap(double wx, double wy, unsigned int &mx, unsigned int &my) const
Convert from world coordinates to map coordinates.
Definition: costmap_2d.cpp:292
A costmap layer base class for costmap plugin layers. Rather than just a layer, this object also cont...
Service to determine if a path is still valid given the current costmap state.
IsPathValidService(nav2::LifecycleNode::WeakPtr node, std::shared_ptr< nav2_costmap_2d::Costmap2DROS > costmap_ros, const rclcpp::Duration &costmap_update_timeout)
Constructor for IsPathValidService.
void initialize()
Initialize the service.
bool makeFootprintFromString(const std::string &footprint_string, std::vector< geometry_msgs::msg::Point > &footprint)
Make the footprint from the given string.
Definition: footprint.cpp:177