Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
goal_intent_extractor.cpp
1 // Copyright (c) 2025, Open Navigation LLC
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 <string>
16 #include <memory>
17 #include <mutex>
18 #include <vector>
19 
20 #include "nav2_route/goal_intent_extractor.hpp"
21 #include "nav2_ros_common/tf2_factories.hpp"
22 
23 namespace nav2_route
24 {
25 
26 static float EPSILON = 1e-6;
27 
29  nav2::LifecycleNode::SharedPtr node,
30  Graph & graph,
31  GraphToIDMap * id_to_graph_map,
32  nav2::TransformBuffer::SharedPtr tf,
33  std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> costmap_subscriber,
34  const std::string & route_frame,
35  const std::string & base_frame)
36 {
37  logger_ = node->get_logger();
38  id_to_graph_map_ = id_to_graph_map;
39  graph_ = &graph;
40  tf_ = tf;
41  costmap_subscriber_ = costmap_subscriber;
42  route_frame_ = route_frame;
43  base_frame_ = base_frame;
44  node_spatial_tree_ = std::make_shared<NodeSpatialTree>();
45  node_spatial_tree_->computeTree(graph);
46 
47  prune_goal_ = node->declare_or_get_parameter("prune_goal", true);
48 
49  max_dist_from_edge_ = static_cast<float>(
50  node->declare_or_get_parameter("max_prune_dist_from_edge", 8.0));
51  min_dist_from_goal_ = static_cast<float>(
52  node->declare_or_get_parameter("min_prune_dist_from_goal", 0.15));
53  min_dist_from_start_ = static_cast<float>(
54  node->declare_or_get_parameter("min_prune_dist_from_start", 0.10));
55 
56  enable_search_ = node->declare_or_get_parameter("enable_nn_search", true);
57  max_nn_search_iterations_ = node->declare_or_get_parameter(
58  "max_nn_search_iterations", 10000);
59 
60  int num_of_nearest_nodes = node->declare_or_get_parameter("num_nearest_nodes", 5);
61  node_spatial_tree_->setNumOfNearestNodes(num_of_nearest_nodes);
62 }
63 
64 void GoalIntentExtractor::setGraph(Graph & graph, GraphToIDMap * id_to_graph_map)
65 {
66  id_to_graph_map_ = id_to_graph_map;
67  graph_ = &graph;
68  node_spatial_tree_->computeTree(graph);
69 }
70 
71 geometry_msgs::msg::PoseStamped GoalIntentExtractor::transformPose(
72  geometry_msgs::msg::PoseStamped & pose,
73  const std::string & target_frame)
74 {
75  if (pose.header.frame_id != target_frame) {
76  RCLCPP_INFO(
77  logger_,
78  "Request pose in %s frame. Converting to route server frame: %s.",
79  pose.header.frame_id.c_str(), target_frame.c_str());
80  if (!nav2_util::transformPoseInTargetFrame(pose, pose, *tf_, target_frame)) {
81  throw nav2_core::RouteTFError("Failed to transform starting pose to: " + target_frame);
82  }
83  }
84  return pose;
85 }
86 
87 void GoalIntentExtractor::overrideStart(const geometry_msgs::msg::PoseStamped & start_pose)
88 {
89  // Override the start pose when rerouting is requested, using the current pose
90  start_ = start_pose;
91 }
92 
93 template<typename GoalT>
94 NodeExtents
95 GoalIntentExtractor::findStartandGoal(const std::shared_ptr<const GoalT> goal)
96 {
97  // If not using the poses, then use the requests Node IDs to establish start and goal
98  if (!goal->use_poses) {
99  unsigned int start_idx = id_to_graph_map_->at(goal->start_id);
100  unsigned int goal_idx = id_to_graph_map_->at(goal->goal_id);
101  const Coordinates & start_coords = graph_->at(start_idx).coords;
102  const Coordinates & goal_coords = graph_->at(goal_idx).coords;
103  start_.pose.position.x = start_coords.x;
104  start_.pose.position.y = start_coords.y;
105  goal_.pose.position.x = goal_coords.x;
106  goal_.pose.position.y = goal_coords.y;
107  return {start_idx, goal_idx};
108  }
109 
110  // Find request start pose
111  geometry_msgs::msg::PoseStamped start_pose, goal_pose = goal->goal;
112  if (goal->use_start) {
113  start_pose = goal->start;
114  } else {
115  if (!nav2_util::getCurrentPose(start_pose, *tf_, route_frame_, base_frame_)) {
116  throw nav2_core::RouteTFError("Failed to obtain starting pose in: " + route_frame_);
117  }
118  }
119 
120  // transform to route_frame
121  start_ = transformPose(start_pose, route_frame_);
122  goal_ = transformPose(goal_pose, route_frame_);
123 
124  // Find closest route graph nodes to start and goal to plan between.
125  // Note that these are the location indices in the graph
126  std::vector<unsigned int> start_route, end_route;
127  if (!node_spatial_tree_->findNearestGraphNodesToPose(start_, start_route) ||
128  !node_spatial_tree_->findNearestGraphNodesToPose(goal_, end_route))
129  {
131  "Could not determine node closest to start or goal pose requested!");
132  }
133 
134  unsigned int start_route_loc = start_route.front();
135  unsigned int end_route_loc = end_route.front();
136 
137  // If given cost information, check which of the nearest graph nodes is nearest by
138  // traversability, not just Euclidean distance, in case of obstacles, walls, etc.
139  // However, if the closest node has Line of Sight to the goal, then use that node
140  // skipping the search as we know it is the closest and now optimally traversible node.
141  std::shared_ptr<nav2_costmap_2d::Costmap2D> costmap = nullptr;
142  std::string costmap_frame_id;
143  bool enable_search = enable_search_;
144  if (enable_search) {
145  try {
146  costmap = costmap_subscriber_->getCostmap();
147  costmap_frame_id = costmap_subscriber_->getFrameID();
148  } catch (const std::exception & ex) {
149  enable_search = false;
150  RCLCPP_WARN(
151  logger_,
152  "Failed to get costmap for goal intent extractor: %s. "
153  "Falling back to closest euclidean route node instead.", ex.what());
154  }
155  }
156 
157  if (enable_search && start_route.size() > 1u) {
158  // Convert the nearest node candidates to the costmap frame for search
159  std::vector<geometry_msgs::msg::PoseStamped> candidate_nodes;
160  candidate_nodes.reserve(start_route.size());
161  for (const auto & node : start_route) {
162  auto & node_data = graph_->at(node);
163  geometry_msgs::msg::PoseStamped node_pose;
164  node_pose.pose.position.x = node_data.coords.x;
165  node_pose.pose.position.y = node_data.coords.y;
166  node_pose.header.frame_id = node_data.coords.frame_id;
167  node_pose.header.stamp = start_pose.header.stamp;
168  candidate_nodes.push_back(transformPose(node_pose, costmap_frame_id));
169  }
170 
171  auto transformed_start = transformPose(start_, costmap_frame_id);
172  std::lock_guard<nav2_costmap_2d::Costmap2D::mutex_t> lock(*costmap->getMutex());
173  GoalIntentSearch::LoSCollisionChecker los_checker(costmap);
174  if (los_checker.worldToMap(
175  candidate_nodes.front().pose.position, transformed_start.pose.position))
176  {
177  if (los_checker.isInCollision()) {
179  if (bfs.search(transformed_start, candidate_nodes, max_nn_search_iterations_)) {
180  start_route_loc = start_route[bfs.getClosestNodeIdx()];
181  }
182  }
183  }
184  }
185 
186  if (enable_search && end_route.size() > 1u) {
187  // Convert the nearest node candidates to the costmap frame for search
188  std::vector<geometry_msgs::msg::PoseStamped> candidate_nodes;
189  candidate_nodes.reserve(end_route.size());
190  for (const auto & node : end_route) {
191  auto & node_data = graph_->at(node);
192  geometry_msgs::msg::PoseStamped node_pose;
193  node_pose.pose.position.x = node_data.coords.x;
194  node_pose.pose.position.y = node_data.coords.y;
195  node_pose.header.frame_id = node_data.coords.frame_id;
196  node_pose.header.stamp = goal_pose.header.stamp;
197  candidate_nodes.push_back(transformPose(node_pose, costmap_frame_id));
198  }
199 
200  auto transformed_end = transformPose(goal_, costmap_frame_id);
201  std::lock_guard<nav2_costmap_2d::Costmap2D::mutex_t> lock(*costmap->getMutex());
202  GoalIntentSearch::LoSCollisionChecker los_checker(costmap);
203  if (los_checker.worldToMap(
204  candidate_nodes.front().pose.position, transformed_end.pose.position))
205  {
206  if (los_checker.isInCollision()) {
208  if (bfs.search(transformed_end, candidate_nodes)) {
209  end_route_loc = end_route[bfs.getClosestNodeIdx()];
210  }
211  }
212  }
213  }
214 
215  return {start_route_loc, end_route_loc};
216 }
217 
218 template<typename GoalT>
220  const Route & input_route,
221  const std::shared_ptr<const GoalT> goal,
222  ReroutingState & rerouting_info)
223 {
224  Route pruned_route = input_route;
225 
226  // Grab and update the rerouting state
227  EdgePtr last_curr_edge = rerouting_info.curr_edge;
228  rerouting_info.curr_edge = nullptr;
229  bool first_time = rerouting_info.first_time;
230  rerouting_info.first_time = false;
231 
232  // Cannot prune if no edges to prune or if using nodeIDs in the initial request (no effect)
233  if (input_route.edges.empty() || (!goal->use_poses && first_time)) {
234  return pruned_route;
235  }
236 
237  // Check on pruning the start node
238  NodePtr first = pruned_route.start_node;
239  NodePtr next = pruned_route.edges[0]->end;
240  float vrx = next->coords.x - first->coords.x;
241  float vry = next->coords.y - first->coords.y;
242  float vpx = start_.pose.position.x - first->coords.x;
243  float vpy = start_.pose.position.y - first->coords.y;
244  float dot_prod = utils::normalizedDot(vrx, vry, vpx, vpy);
245  Coordinates closest_pt_on_edge = utils::findClosestPoint(start_, first->coords, next->coords);
246  if (dot_prod > EPSILON && // A projection exists
247  hypotf(vpx, vpy) > min_dist_from_start_ && // We're not on the node to prune entire edge
248  utils::distance(closest_pt_on_edge, start_) <= max_dist_from_edge_) // Close enough to edge
249  {
250  // Record the pruned edge information if its the same edge as previously routed so that
251  // the tracker can seed this information into its state to proceed with its task losslessly
252  if (last_curr_edge && last_curr_edge->edgeid == pruned_route.edges.front()->edgeid) {
253  rerouting_info.closest_pt_on_edge = closest_pt_on_edge;
254  rerouting_info.curr_edge = pruned_route.edges.front();
255  }
256 
257  pruned_route.start_node = next;
258  pruned_route.route_cost -= pruned_route.edges.front()->end->search_state.traversal_cost;
259  pruned_route.edges.erase(pruned_route.edges.begin());
260  }
261 
262  // Don't prune the goal if requested, if given a known goal_id (no effect), or now empty
263  if (!prune_goal_ || !goal->use_poses || pruned_route.edges.empty()) {
264  return pruned_route;
265  }
266 
267  // Check on pruning the goal node
268  next = pruned_route.edges.back()->start;
269  NodePtr last = pruned_route.edges.back()->end;
270  vrx = last->coords.x - next->coords.x;
271  vry = last->coords.y - next->coords.y;
272  vpx = goal_.pose.position.x - last->coords.x;
273  vpy = goal_.pose.position.y - last->coords.y;
274 
275  dot_prod = utils::normalizedDot(vrx, vry, vpx, vpy);
276  closest_pt_on_edge = utils::findClosestPoint(goal_, next->coords, last->coords);
277  if (dot_prod < -EPSILON && // A projection exists
278  hypotf(vpx, vpy) > min_dist_from_goal_ && // We're not on the node to prune entire edge
279  utils::distance(closest_pt_on_edge, goal_) <= max_dist_from_edge_) // Close enough to edge
280  {
281  pruned_route.route_cost -= pruned_route.edges.back()->end->search_state.traversal_cost;
282  pruned_route.edges.pop_back();
283  }
284 
285  return pruned_route;
286 }
287 
288 geometry_msgs::msg::PoseStamped GoalIntentExtractor::getStart()
289 {
290  return start_;
291 }
292 
293 template Route GoalIntentExtractor::pruneStartandGoal<nav2_msgs::action::ComputeRoute::Goal>(
294  const Route & input_route,
295  const std::shared_ptr<const nav2_msgs::action::ComputeRoute::Goal> goal,
296  ReroutingState & rerouting_info);
297 template
298 Route GoalIntentExtractor::pruneStartandGoal<nav2_msgs::action::ComputeAndTrackRoute::Goal>(
299  const Route & input_route,
300  const std::shared_ptr<const nav2_msgs::action::ComputeAndTrackRoute::Goal> goal,
301  ReroutingState & rerouting_info);
302 template NodeExtents GoalIntentExtractor::findStartandGoal<nav2_msgs::action::ComputeRoute::Goal>(
303  const std::shared_ptr<const nav2_msgs::action::ComputeRoute::Goal> goal);
304 template
305 NodeExtents GoalIntentExtractor::findStartandGoal<nav2_msgs::action::ComputeAndTrackRoute::Goal>(
306  const std::shared_ptr<const nav2_msgs::action::ComputeAndTrackRoute::Goal> goal);
307 
308 } // namespace nav2_route
geometry_msgs::msg::PoseStamped transformPose(geometry_msgs::msg::PoseStamped &pose, const std::string &frame_id)
Transforms a pose into the route frame.
void setGraph(Graph &graph, GraphToIDMap *id_to_graph_map)
Sets a new graph when updated.
Route pruneStartandGoal(const Route &input_route, const std::shared_ptr< const GoalT > goal, ReroutingState &rerouting_info)
Prune the start and end nodes in a route if the start or goal poses, respectively,...
void overrideStart(const geometry_msgs::msg::PoseStamped &start_pose)
Override the start pose for use in pruning if it is externally overridden Usually by the rerouting lo...
void configure(nav2::LifecycleNode::SharedPtr node, Graph &graph, GraphToIDMap *id_to_graph_map, nav2::TransformBuffer::SharedPtr tf, std::shared_ptr< nav2_costmap_2d::CostmapSubscriber > costmap_subscriber, const std::string &route_frame, const std::string &base_frame)
Configure extractor.
NodeExtents findStartandGoal(const std::shared_ptr< const GoalT > goal)
Main API to find the start and goal graph IDX (not IDs) for routing.
geometry_msgs::msg::PoseStamped getStart()
gets the desired start pose
bool search(const geometry_msgs::msg::PoseStamped &reference_node, const std::vector< geometry_msgs::msg::PoseStamped > &candidate_nodes, const int max_iterations=std::numeric_limits< int >::max())
Search for the closest node to the given reference node.
unsigned int getClosestNodeIdx()
Get the output closest node in candidate indices.
bool isInCollision()
Check if the line segment is in collision with the costmap.
bool worldToMap(const geometry_msgs::msg::Point &start, const geometry_msgs::msg::Point &end)
Find the line segment in cosmap frame.
An object to store Node coordinates in different frames.
Definition: types.hpp:173
An object representing edges between nodes.
Definition: types.hpp:134
An object to store the nodes in the graph file.
Definition: types.hpp:183
State shared to objects to communicate important rerouting data to avoid rerouting over blocked edges...
Definition: types.hpp:264
An ordered set of nodes and edges corresponding to the planned route.
Definition: types.hpp:211