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