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