diff --git a/nav2_costmap_2d/CMakeLists.txt b/nav2_costmap_2d/CMakeLists.txt
index 00cb720aa15..5bfaa70bc90 100644
--- a/nav2_costmap_2d/CMakeLists.txt
+++ b/nav2_costmap_2d/CMakeLists.txt
@@ -73,6 +73,7 @@ add_library(layers SHARED
plugins/range_sensor_layer.cpp
plugins/denoise_layer.cpp
plugins/plugin_container_layer.cpp
+ plugins/tracking_error_layer.cpp
)
target_include_directories(layers
PUBLIC
diff --git a/nav2_costmap_2d/costmap_plugins.xml b/nav2_costmap_2d/costmap_plugins.xml
index 6d2bde4a1c3..5647cfb91ee 100644
--- a/nav2_costmap_2d/costmap_plugins.xml
+++ b/nav2_costmap_2d/costmap_plugins.xml
@@ -12,6 +12,9 @@
Similar to obstacle costmap, but uses 3D voxel grid to store data.
+
+ Creates a corridor around the path.
+
A range-sensor (sonar, IR) based obstacle layer for costmap_2d
diff --git a/nav2_costmap_2d/include/nav2_costmap_2d/tracking_error_layer.hpp b/nav2_costmap_2d/include/nav2_costmap_2d/tracking_error_layer.hpp
new file mode 100644
index 00000000000..2a286217a8f
--- /dev/null
+++ b/nav2_costmap_2d/include/nav2_costmap_2d/tracking_error_layer.hpp
@@ -0,0 +1,91 @@
+// Copyright (c) 2025 Berkan Tali
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NAV2_COSTMAP_2D__TRACKING_ERROR_LAYER_HPP_
+#define NAV2_COSTMAP_2D__TRACKING_ERROR_LAYER_HPP_
+
+#include
+#include
+#include
+
+#include "nav2_costmap_2d/layered_costmap.hpp"
+#include "rclcpp/rclcpp.hpp"
+#include "nav2_costmap_2d/layer.hpp"
+#include "nav_msgs/msg/path.hpp"
+#include "nav2_msgs/msg/tracking_feedback.hpp"
+#include "nav2_costmap_2d/costmap_layer.hpp"
+#include "nav2_util/path_utils.hpp"
+#include "nav2_util/geometry_utils.hpp"
+#include "nav2_util/robot_utils.hpp"
+#include
+#include "tf2_ros/buffer.hpp"
+#include "tf2_ros/transform_listener.hpp"
+#include "geometry_msgs/msg/pose_stamped.hpp"
+
+
+namespace nav2_costmap_2d
+{
+
+class TrackingErrorLayer : public nav2_costmap_2d::CostmapLayer
+{
+public:
+ TrackingErrorLayer();
+
+ ~TrackingErrorLayer();
+ virtual void onInitialize();
+ virtual void updateBounds(
+ double robot_x, double robot_y, double robot_yaw, double * min_x,
+ double * min_y, double * max_x, double * max_y);
+ virtual void updateCosts(
+ nav2_costmap_2d::Costmap2D & master_grid,
+ int min_i, int min_j, int max_i, int max_j);
+ virtual void reset();
+ virtual void onFootprintChanged();
+ virtual bool isClearable() {return false;}
+
+ // Lifecycle methods
+ virtual void activate();
+ virtual void deactivate();
+ virtual void cleanup();
+ std::vector> getWallPoints(const nav_msgs::msg::Path & segment);
+ nav_msgs::msg::Path getPathSegment();
+
+protected:
+ void pathCallback(const nav_msgs::msg::Path::SharedPtr msg);
+ void trackingCallback(const nav2_msgs::msg::TrackingFeedback::SharedPtr msg);
+ nav2_msgs::msg::TrackingFeedback last_tracking_feedback_;
+ rcl_interfaces::msg::SetParametersResult
+ dynamicParametersCallback(std::vector parameters);
+
+private:
+ nav2::Subscription::SharedPtr path_sub_;
+ nav2::Subscription::SharedPtr tracking_feedback_sub_;
+ std::mutex path_mutex_;
+ std::mutex tracking_error_mutex_;
+ nav_msgs::msg::Path last_path_;
+ rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr dyn_params_handler_;
+
+ std::shared_ptr tf_buffer_;
+ std::shared_ptr tf_listener_;
+
+ size_t temp_step_;
+ int step_;
+ double width_;
+ double look_ahead_;
+ bool enabled_;
+};
+
+} // namespace nav2_costmap_2d
+
+#endif // NAV2_COSTMAP_2D__TRACKING_ERROR_LAYER_HPP_
diff --git a/nav2_costmap_2d/plugins/tracking_error_layer.cpp b/nav2_costmap_2d/plugins/tracking_error_layer.cpp
new file mode 100644
index 00000000000..dab13229e7a
--- /dev/null
+++ b/nav2_costmap_2d/plugins/tracking_error_layer.cpp
@@ -0,0 +1,361 @@
+// Copyright (c) 2025 Berkan Tali
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "nav2_costmap_2d/tracking_error_layer.hpp"
+#include "pluginlib/class_list_macros.hpp"
+
+PLUGINLIB_EXPORT_CLASS(nav2_costmap_2d::TrackingErrorLayer, nav2_costmap_2d::Layer)
+
+namespace nav2_costmap_2d
+{
+
+TrackingErrorLayer::TrackingErrorLayer() {}
+
+void TrackingErrorLayer::onInitialize()
+{
+ auto node = node_.lock();
+ if (!node) {
+ throw std::runtime_error{"Failed to lock node"};
+ }
+
+ // Declare and get parameters
+ declareParameter("look_ahead", rclcpp::ParameterValue(5.0));
+ node->get_parameter(name_ + "." + "look_ahead", look_ahead_);
+
+ declareParameter("step", rclcpp::ParameterValue(5));
+ node->get_parameter(name_ + "." + "step", step_);
+ temp_step_ = static_cast(step_);
+
+ declareParameter("corridor_width", rclcpp::ParameterValue(2.0));
+ node->get_parameter(name_ + "." + "corridor_width", width_);
+
+ declareParameter("enabled", rclcpp::ParameterValue(true));
+ node->get_parameter(name_ + "." + "enabled", enabled_);
+ if (look_ahead_ <= 0.0) {
+ throw std::runtime_error{"look_ahead must be positive"};
+ }
+ if (width_ <= 0.0) {
+ throw std::runtime_error{"corridor_width must be positive"};
+ }
+ if (temp_step_ == 0) {
+ throw std::runtime_error{"step must be greater than zero"};
+ }
+ dyn_params_handler_ = node->add_on_set_parameters_callback(
+ std::bind(
+ &TrackingErrorLayer::dynamicParametersCallback,
+ this, std::placeholders::_1));
+
+ path_sub_ = node->create_subscription(
+ "/plan",
+ std::bind(&TrackingErrorLayer::pathCallback, this, std::placeholders::_1),
+ rclcpp::QoS(10).reliable()
+ );
+
+ tracking_feedback_sub_ = node->create_subscription(
+ "/tracking_feedback",
+ std::bind(&TrackingErrorLayer::trackingCallback, this, std::placeholders::_1),
+ rclcpp::QoS(10).reliable()
+ );
+ tf_buffer_ = std::make_shared(node->get_clock());
+ tf_buffer_->setUsingDedicatedThread(true);
+ tf_listener_ = std::make_shared(*tf_buffer_);
+}
+
+void TrackingErrorLayer::pathCallback(const nav_msgs::msg::Path::SharedPtr msg)
+{
+ std::lock_guard lock(path_mutex_);
+ last_path_ = *msg;
+}
+
+void TrackingErrorLayer::trackingCallback(
+ const nav2_msgs::msg::TrackingFeedback::SharedPtr msg)
+{
+ std::lock_guard lock(tracking_error_mutex_);
+ last_tracking_feedback_ = *msg;
+}
+
+void TrackingErrorLayer::updateBounds(
+ double robot_x, double robot_y, double /*robot_yaw*/,
+ double * min_x, double * min_y, double * max_x, double * max_y)
+{
+ std::lock_guard guard(*getMutex());
+ if (!enabled_) {
+ return;
+ }
+ useExtraBounds(min_x, min_y, max_x, max_y);
+ *min_x = std::min(*min_x, robot_x - look_ahead_);
+ *max_x = std::max(*max_x, robot_x + look_ahead_);
+ *min_y = std::min(*min_y, robot_y - look_ahead_);
+ *max_y = std::max(*max_y, robot_y + look_ahead_);
+}
+
+std::vector> TrackingErrorLayer::getWallPoints(
+ const nav_msgs::msg::Path & segment)
+{
+ std::vector> point_list;
+
+ // Early return checks
+ if (segment.poses.empty() || temp_step_ == 0) {
+ return point_list;
+ }
+
+ // Reserve space for better performance (estimate 2 points per step)
+ size_t estimated_points = (segment.poses.size() / temp_step_) * 2;
+ point_list.reserve(estimated_points);
+
+ // Process path points with step size
+ for (size_t current_index = 0; current_index < segment.poses.size();
+ current_index += temp_step_)
+ {
+ const auto & current_pose = segment.poses[current_index];
+ double px = current_pose.pose.position.x;
+ double py = current_pose.pose.position.y;
+
+ // Calculate direction vector to next point
+ double dx = 0.0, dy = 0.0;
+
+ if (current_index + temp_step_ < segment.poses.size()) {
+ // Use next point at step distance
+ const auto & next_pose = segment.poses[current_index + temp_step_];
+ dx = next_pose.pose.position.x - px;
+ dy = next_pose.pose.position.y - py;
+ } else if (current_index + 1 < segment.poses.size()) {
+ // Use immediate next point if step would go out of bounds
+ const auto & next_pose = segment.poses[current_index + 1];
+ dx = next_pose.pose.position.x - px;
+ dy = next_pose.pose.position.y - py;
+ } else {
+ // Last point - use previous direction if available
+ if (current_index > 0) {
+ const auto & prev_pose = segment.poses[current_index - 1];
+ dx = px - prev_pose.pose.position.x;
+ dy = py - prev_pose.pose.position.y;
+ } else {
+ continue;
+ }
+ }
+
+ // Calculate perpendicular direction for wall points
+ double norm = std::hypot(dx, dy);
+ if (norm < 1e-6) {
+ continue;
+ }
+
+ double perp_x = -dy / norm;
+ double perp_y = dx / norm;
+
+ double half_width = width_ * 0.5;
+
+ point_list.push_back({px + perp_x * half_width, py + perp_y * half_width});
+ point_list.push_back({px - perp_x * half_width, py - perp_y * half_width});
+ }
+
+ return point_list;
+}
+
+
+nav_msgs::msg::Path TrackingErrorLayer::getPathSegment()
+{
+ std::lock_guard path_lock(path_mutex_);
+ std::lock_guard error_lock(tracking_error_mutex_);
+
+ nav_msgs::msg::Path segment;
+ if (last_path_.poses.empty() ||
+ last_tracking_feedback_.current_path_index >= last_path_.poses.size())
+ {
+ return segment;
+ }
+
+ size_t start_index = last_tracking_feedback_.current_path_index;
+ size_t end_index = start_index;
+ double dist_traversed = 0.0;
+
+ // Limit the corridor with look_ahead_
+ for (size_t i = start_index; i < last_path_.poses.size() - 1; ++i) {
+ dist_traversed += nav2_util::geometry_utils::euclidean_distance(
+ last_path_.poses[i], last_path_.poses[i + 1]);
+ end_index = i + 1;
+ if (dist_traversed >= look_ahead_) {
+ break;
+ }
+ }
+
+ // Create the forward corridor segment
+ if (start_index < end_index && end_index <= last_path_.poses.size()) {
+ segment.header = last_path_.header;
+ segment.poses.assign(
+ last_path_.poses.begin() + start_index,
+ last_path_.poses.begin() + end_index
+ );
+ } else if (start_index == last_path_.poses.size() - 1) {
+ segment.header = last_path_.header;
+ segment.poses.push_back(last_path_.poses.back());
+ }
+
+ return segment;
+}
+
+void TrackingErrorLayer::updateCosts(
+ nav2_costmap_2d::Costmap2D & master_grid,
+ int /*min_i*/, int /*min_j*/, int /*max_i*/, int /*max_j*/)
+{
+ std::lock_guard guard(*getMutex());
+
+ if (!enabled_) {
+ return;
+ }
+
+ nav_msgs::msg::Path segment = getPathSegment();
+ if (segment.poses.size() < 2) {
+ return;
+ }
+
+ std::string costmap_frame = layered_costmap_->getGlobalFrameID();
+ nav_msgs::msg::Path transformed_segment;
+ transformed_segment.header.frame_id = costmap_frame;
+
+ for (const auto & pose : segment.poses) {
+ geometry_msgs::msg::PoseStamped transformed_pose;
+ if (nav2_util::transformPoseInTargetFrame(
+ pose, transformed_pose, *tf_buffer_, costmap_frame, 0.1))
+ {
+ transformed_segment.poses.push_back(transformed_pose);
+ }
+ }
+
+ auto wall_points = getWallPoints(transformed_segment);
+
+ // Separate left and right wall points
+ std::vector> left_wall_points, right_wall_points;
+ for (size_t i = 0; i < wall_points.size(); i += 2) {
+ if (i + 1 < wall_points.size()) {
+ left_wall_points.push_back(wall_points[i]); // Even indices = left
+ right_wall_points.push_back(wall_points[i + 1]); // Odd indices = right
+ }
+ }
+
+ // Use Bresenham to get consistent lines
+ for (size_t i = 1; i < left_wall_points.size(); ++i) {
+ double x0 = left_wall_points[i - 1][0];
+ double y0 = left_wall_points[i - 1][1];
+ double x1 = left_wall_points[i][0];
+ double y1 = left_wall_points[i][1];
+
+ unsigned int map_x0, map_y0, map_x1, map_y1;
+ if (master_grid.worldToMap(x0, y0, map_x0, map_y0) &&
+ master_grid.worldToMap(x1, y1, map_x1, map_y1))
+ {
+ auto cells = nav2_util::geometry_utils::bresenhamLine(
+ static_cast(map_x0), static_cast(map_y0),
+ static_cast(map_x1), static_cast(map_y1));
+
+ for (const auto & cell : cells) {
+ master_grid.setCost(cell.first, cell.second, nav2_costmap_2d::LETHAL_OBSTACLE);
+ }
+ }
+ }
+
+ for (size_t i = 1; i < right_wall_points.size(); ++i) {
+ double x0 = right_wall_points[i - 1][0];
+ double y0 = right_wall_points[i - 1][1];
+ double x1 = right_wall_points[i][0];
+ double y1 = right_wall_points[i][1];
+
+ unsigned int map_x0, map_y0, map_x1, map_y1;
+ if (master_grid.worldToMap(x0, y0, map_x0, map_y0) &&
+ master_grid.worldToMap(x1, y1, map_x1, map_y1))
+ {
+ auto cells = nav2_util::geometry_utils::bresenhamLine(
+ static_cast(map_x0), static_cast(map_y0),
+ static_cast(map_x1), static_cast(map_y1));
+
+ for (const auto & cell : cells) {
+ master_grid.setCost(cell.first, cell.second, nav2_costmap_2d::LETHAL_OBSTACLE);
+ }
+ }
+ }
+}
+
+rcl_interfaces::msg::SetParametersResult TrackingErrorLayer::dynamicParametersCallback(
+ std::vector parameters)
+{
+ std::lock_guard guard(*getMutex());
+ rcl_interfaces::msg::SetParametersResult result;
+ result.successful = true;
+
+ for (auto parameter : parameters) {
+ const auto & param_type = parameter.get_type();
+ const auto & param_name = parameter.get_name();
+
+ // Only process parameters for this layer
+ if (param_name.find(name_ + ".") != 0) {
+ continue;
+ }
+
+ if (param_type == rcl_interfaces::msg::ParameterType::PARAMETER_DOUBLE) {
+ if (param_name == name_ + "." + "look_ahead") {
+ double new_value = parameter.as_double();
+ if (new_value <= 0.0) {
+ result.successful = false;
+ result.reason = "look_ahead must be positive";
+ return result;
+ }
+ look_ahead_ = new_value;
+ } else if (param_name == name_ + "." + "corridor_width") {
+ double new_value = parameter.as_double();
+ if (new_value <= 0.0) {
+ result.successful = false;
+ result.reason = "corridor_width must be positive";
+ return result;
+ }
+ width_ = new_value;
+ }
+ } else if (param_type == rcl_interfaces::msg::ParameterType::PARAMETER_BOOL) {
+ if (param_name == name_ + "." + "enabled") {
+ enabled_ = parameter.as_bool();
+ }
+ } else if (param_type == rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER) {
+ if (param_name == name_ + "." + "step") {
+ int new_value = parameter.as_int();
+ if (new_value <= 0) {
+ result.successful = false;
+ result.reason = "step must be greater than zero";
+ return result;
+ }
+ step_ = new_value;
+ temp_step_ = static_cast(step_);
+ }
+ }
+ }
+
+ return result;
+}
+
+TrackingErrorLayer::~TrackingErrorLayer()
+{
+ auto node = node_.lock();
+ if (dyn_params_handler_ && node) {
+ node->remove_on_set_parameters_callback(dyn_params_handler_.get());
+ }
+ dyn_params_handler_.reset();
+}
+
+void TrackingErrorLayer::reset() {}
+void TrackingErrorLayer::activate() {enabled_ = true;}
+void TrackingErrorLayer::deactivate() {enabled_ = false;}
+
+void TrackingErrorLayer::onFootprintChanged() {}
+void TrackingErrorLayer::cleanup() {}
+
+} // namespace nav2_costmap_2d
diff --git a/nav2_costmap_2d/test/unit/CMakeLists.txt b/nav2_costmap_2d/test/unit/CMakeLists.txt
index 559ef4ae4e2..9d9f3e9f959 100644
--- a/nav2_costmap_2d/test/unit/CMakeLists.txt
+++ b/nav2_costmap_2d/test/unit/CMakeLists.txt
@@ -67,3 +67,9 @@ ament_add_gtest(coordinate_transform_test coordinate_transform_test.cpp)
target_link_libraries(coordinate_transform_test
nav2_costmap_2d_core
)
+
+ament_add_gtest(tracking_error_layer_test tracking_error_layer_test.cpp)
+target_link_libraries(tracking_error_layer_test
+ nav2_costmap_2d_core
+ layers
+)
diff --git a/nav2_costmap_2d/test/unit/tracking_error_layer_test.cpp b/nav2_costmap_2d/test/unit/tracking_error_layer_test.cpp
new file mode 100644
index 00000000000..167f4fbfa38
--- /dev/null
+++ b/nav2_costmap_2d/test/unit/tracking_error_layer_test.cpp
@@ -0,0 +1,422 @@
+// Copyright (c) 2025 Berkan Tali
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "nav2_ros_common/lifecycle_node.hpp"
+#include "nav2_costmap_2d/tracking_error_layer.hpp"
+#include "nav2_costmap_2d/layered_costmap.hpp"
+#include "nav2_costmap_2d/costmap_2d.hpp"
+#include "nav_msgs/msg/path.hpp"
+#include "nav2_msgs/msg/tracking_feedback.hpp"
+#include "geometry_msgs/msg/pose_stamped.hpp"
+#include "rcutils/logging.h"
+// Test constants
+static constexpr double TEST_CORRIDOR_WIDTH = 2.0;
+static constexpr int TEST_PATH_POINTS = 50;
+static constexpr double TEST_PATH_LENGTH = 5.0;
+
+class TrackingErrorLayerWrapper : public nav2_costmap_2d::TrackingErrorLayer
+{
+public:
+ using TrackingErrorLayer::last_tracking_feedback_;
+ using TrackingErrorLayer::pathCallback;
+ using TrackingErrorLayer::trackingCallback;
+ using TrackingErrorLayer::getPathSegment;
+ using TrackingErrorLayer::getWallPoints;
+};
+
+class TrackingErrorLayerTestFixture : public ::testing::Test
+{
+public:
+ TrackingErrorLayerTestFixture()
+ {
+ node_ = std::make_shared("test_tracking_error_layer");
+ node_->trigger_transition(lifecycle_msgs::msg::Transition::TRANSITION_CONFIGURE);
+ node_->trigger_transition(lifecycle_msgs::msg::Transition::TRANSITION_ACTIVATE);
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ rclcpp::executors::SingleThreadedExecutor exec;
+ exec.spin_some();
+ costmap_ = std::make_shared(100, 100, 0.05, -2.5, -2.5);
+ layered_costmap_ = std::make_shared("map", false, false);
+ layered_costmap_->resizeMap(100, 100, 0.05, -2.5, -2.5);
+
+ layer_ = std::make_shared();
+ layer_->initialize(layered_costmap_.get(), "test_layer", nullptr, node_, nullptr);
+ layer_->last_tracking_feedback_.current_path_index = static_cast(-1);
+ }
+
+ void SetUp() override
+ {
+ layer_->activate();
+ }
+
+ void TearDown() override
+ {
+ layer_->deactivate();
+ if (node_->get_current_state().id() != lifecycle_msgs::msg::State::PRIMARY_STATE_FINALIZED) {
+ node_->trigger_transition(lifecycle_msgs::msg::Transition::TRANSITION_DEACTIVATE);
+ node_->trigger_transition(lifecycle_msgs::msg::Transition::TRANSITION_CLEANUP);
+ node_->trigger_transition(lifecycle_msgs::msg::Transition::TRANSITION_UNCONFIGURED_SHUTDOWN);
+ }
+ }
+
+protected:
+ nav2::LifecycleNode::SharedPtr node_;
+ std::shared_ptr costmap_;
+ std::shared_ptr layered_costmap_;
+ std::shared_ptr layer_;
+
+ nav_msgs::msg::Path createStraightPath(
+ double start_x = 0.0, double start_y = 0.0,
+ double length = TEST_PATH_LENGTH, int num_points = TEST_PATH_POINTS)
+ {
+ nav_msgs::msg::Path path;
+ path.header.frame_id = "map";
+ path.header.stamp = node_->now();
+
+ for (int i = 0; i < num_points; ++i) {
+ geometry_msgs::msg::PoseStamped pose;
+ pose.header = path.header;
+ pose.pose.position.x = start_x + (i * length / num_points);
+ pose.pose.position.y = start_y;
+ pose.pose.position.z = 0.0;
+ pose.pose.orientation.w = 1.0;
+ path.poses.push_back(pose);
+ }
+ return path;
+ }
+
+ nav_msgs::msg::Path createCurvedPath()
+ {
+ nav_msgs::msg::Path path;
+ path.header.frame_id = "map";
+ path.header.stamp = node_->now();
+
+ for (int i = 0; i <= 50; ++i) {
+ geometry_msgs::msg::PoseStamped pose;
+ pose.header = path.header;
+ double angle = M_PI * i / 50.0;
+ pose.pose.position.x = 2.0 * cos(angle);
+ pose.pose.position.y = 2.0 * sin(angle);
+ pose.pose.position.z = 0.0;
+ pose.pose.orientation.w = 1.0;
+ path.poses.push_back(pose);
+ }
+ return path;
+ }
+
+ nav2_msgs::msg::TrackingFeedback createTrackingFeedback(
+ uint32_t path_index = 0,
+ double error = 0.1)
+ {
+ nav2_msgs::msg::TrackingFeedback feedback;
+ feedback.header.frame_id = "map";
+ feedback.header.stamp = node_->now();
+ feedback.current_path_index = path_index;
+ feedback.tracking_error = error;
+
+ feedback.robot_pose.header = feedback.header;
+ feedback.robot_pose.pose.position.x = path_index * 0.1;
+ feedback.robot_pose.pose.position.y = 0.0;
+ feedback.robot_pose.pose.orientation.w = 1.0;
+
+ return feedback;
+ }
+
+ bool hasObstacles(const nav2_costmap_2d::Costmap2D & costmap)
+ {
+ for (unsigned int i = 0; i < costmap.getSizeInCellsX(); ++i) {
+ for (unsigned int j = 0; j < costmap.getSizeInCellsY(); ++j) {
+ if (costmap.getCost(i, j) == nav2_costmap_2d::LETHAL_OBSTACLE) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+};
+
+// Basic initialization and lifecycle tests
+TEST_F(TrackingErrorLayerTestFixture, test_initialization_and_parameters)
+{
+ EXPECT_TRUE(layer_ != nullptr);
+ EXPECT_FALSE(layer_->isClearable());
+ EXPECT_NO_THROW(layer_->activate());
+ EXPECT_NO_THROW(layer_->deactivate());
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_lifecycle_methods)
+{
+ EXPECT_NO_THROW(layer_->activate());
+ EXPECT_NO_THROW(layer_->deactivate());
+ EXPECT_NO_THROW(layer_->reset());
+ EXPECT_NO_THROW(layer_->onFootprintChanged());
+ EXPECT_NO_THROW(layer_->cleanup());
+}
+
+// Empty data handling tests
+TEST_F(TrackingErrorLayerTestFixture, test_empty_data_handling)
+{
+ nav2_costmap_2d::Costmap2D master_grid(100, 100, 0.05, -2.5, -2.5);
+ EXPECT_NO_THROW(layer_->updateCosts(master_grid, 0, 0, 100, 100));
+ EXPECT_FALSE(hasObstacles(master_grid));
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_path_segment_no_data)
+{
+ // Test segment extraction with no path
+ auto segment_no_path = layer_->getPathSegment();
+ EXPECT_TRUE(segment_no_path.poses.empty());
+
+ // Test with path but no tracking feedback
+ auto path = createStraightPath();
+ layer_->pathCallback(std::make_shared(path));
+ auto segment_no_tracking = layer_->getPathSegment();
+ EXPECT_TRUE(segment_no_tracking.poses.empty());
+}
+
+// Path segment extraction tests
+TEST_F(TrackingErrorLayerTestFixture, test_path_segment_extraction)
+{
+ auto path = createStraightPath();
+ auto tracking_error = createTrackingFeedback(10);
+ layer_->pathCallback(std::make_shared(path));
+ layer_->trackingCallback(std::make_shared(tracking_error));
+
+ auto segment = layer_->getPathSegment();
+ EXPECT_FALSE(segment.poses.empty());
+ EXPECT_EQ(segment.header.frame_id, "map");
+ EXPECT_GT(segment.poses.size(), 0u);
+ EXPECT_GE(segment.poses[0].pose.position.x, tracking_error.robot_pose.pose.position.x);
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_path_segment_bounds)
+{
+ auto path = createStraightPath(0.0, 0.0, 5.0, 50); // 50 points, 5m long
+ auto tracking_error = createTrackingFeedback(25);
+
+ layer_->pathCallback(std::make_shared(path));
+ layer_->trackingCallback(std::make_shared(tracking_error));
+
+ auto segment = layer_->getPathSegment();
+
+ // Should extract segment around index 25
+ EXPECT_FALSE(segment.poses.empty());
+ EXPECT_LT(segment.poses.size(), path.poses.size());
+
+ // Check segment boundaries make sense
+ double first_x = segment.poses.front().pose.position.x;
+ double last_x = segment.poses.back().pose.position.x;
+ double robot_x = tracking_error.robot_pose.pose.position.x;
+
+ EXPECT_LE(first_x, robot_x);
+ EXPECT_GE(last_x, robot_x);
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_path_segment_edge_cases)
+{
+ auto path = createStraightPath(0.0, 0.0, 5.0, 50);
+
+ // Test near start of path
+ auto tracking_error_start = createTrackingFeedback(2);
+ layer_->pathCallback(std::make_shared(path));
+ layer_->trackingCallback(std::make_shared(
+ tracking_error_start));
+ auto segment_start = layer_->getPathSegment();
+ EXPECT_FALSE(segment_start.poses.empty());
+
+ // Test near end of path
+ auto tracking_error_end = createTrackingFeedback(47);
+ layer_->trackingCallback(std::make_shared(tracking_error_end));
+ auto segment_end = layer_->getPathSegment();
+ EXPECT_FALSE(segment_end.poses.empty());
+
+ // Test exactly at end
+ auto tracking_error_last = createTrackingFeedback(49);
+ layer_->trackingCallback(std::make_shared(tracking_error_last));
+ auto segment_last = layer_->getPathSegment();
+ EXPECT_FALSE(segment_last.poses.empty());
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_path_segment_length_consistency)
+{
+ auto path = createStraightPath(0.0, 0.0, 10.0, 100);
+
+ // Test different positions, segment length should be consistent
+ std::vector test_indices = {10, 25, 50, 75, 90};
+ std::vector segment_sizes;
+
+ layer_->pathCallback(std::make_shared(path));
+
+ for (auto index : test_indices) {
+ auto tracking_error = createTrackingFeedback(index);
+ layer_->trackingCallback(std::make_shared(tracking_error));
+ auto segment = layer_->getPathSegment();
+ segment_sizes.push_back(segment.poses.size());
+ }
+
+ // Segments should have similar sizes
+ for (size_t i = 1; i < segment_sizes.size() - 1; ++i) {
+ EXPECT_GT(segment_sizes[i], 0u);
+ }
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_invalid_path_index)
+{
+ auto path = createStraightPath();
+ auto tracking_error = createTrackingFeedback(1000); // Invalid index
+ layer_->pathCallback(std::make_shared(path));
+ layer_->trackingCallback(std::make_shared(tracking_error));
+ auto segment = layer_->getPathSegment();
+ EXPECT_TRUE(segment.poses.empty());
+}
+
+// Wall points generation tests
+TEST_F(TrackingErrorLayerTestFixture, test_wall_points_straight_path)
+{
+ auto path = createStraightPath();
+ auto wall_points = layer_->getWallPoints(path);
+ EXPECT_FALSE(wall_points.empty());
+ EXPECT_EQ(wall_points.size() % 2, 0u); // Should be pairs (left/right)
+
+ for (const auto & point : wall_points) {
+ EXPECT_EQ(point.size(), 2u); // [x, y]
+ EXPECT_TRUE(std::isfinite(point[0]));
+ EXPECT_TRUE(std::isfinite(point[1]));
+ }
+
+ if (wall_points.size() >= 2) {
+ double left_y = wall_points[0][1];
+ double right_y = wall_points[1][1];
+ EXPECT_TRUE((left_y > 0 && right_y < 0) || (left_y < 0 && right_y > 0));
+ }
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_wall_points_curved_path)
+{
+ auto curved_path = createCurvedPath();
+ auto wall_points = layer_->getWallPoints(curved_path);
+ EXPECT_FALSE(wall_points.empty());
+ EXPECT_EQ(wall_points.size() % 2, 0u);
+
+ if (wall_points.size() >= 4) {
+ double first_left_x = wall_points[0][0];
+ double second_left_x = wall_points[2][0];
+ EXPECT_NE(first_left_x, second_left_x);
+ }
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_single_point_path)
+{
+ nav_msgs::msg::Path single_point_path;
+ single_point_path.header.frame_id = "map";
+ single_point_path.header.stamp = node_->now();
+
+ geometry_msgs::msg::PoseStamped single_pose;
+ single_pose.header = single_point_path.header;
+ single_pose.pose.position.x = 1.0;
+ single_pose.pose.position.y = 1.0;
+ single_pose.pose.orientation.w = 1.0;
+ single_point_path.poses.push_back(single_pose);
+
+ auto wall_points = layer_->getWallPoints(single_point_path);
+ EXPECT_TRUE(wall_points.empty());
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_update_costs_same_frame)
+{
+ auto path = createStraightPath();
+ auto tracking_error = createTrackingFeedback(5);
+ layer_->pathCallback(std::make_shared(path));
+ layer_->trackingCallback(std::make_shared(tracking_error));
+
+ nav2_costmap_2d::Costmap2D master_grid(100, 100, 0.05, -2.5, -2.5);
+ EXPECT_NO_THROW(layer_->updateCosts(master_grid, 0, 0, 100, 100));
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_obstacles_created_in_corridor)
+{
+ auto path = createStraightPath();
+ auto tracking_error = createTrackingFeedback(5);
+ layer_->pathCallback(std::make_shared(path));
+ layer_->trackingCallback(std::make_shared(tracking_error));
+
+ nav2_costmap_2d::Costmap2D master_grid(100, 100, 0.05, -2.5, -2.5);
+ layer_->updateCosts(master_grid, 0, 0, 100, 100);
+
+ // Check that some obstacles were actually created
+ EXPECT_TRUE(hasObstacles(master_grid));
+}
+
+// Edge cases and robustness tests
+TEST_F(TrackingErrorLayerTestFixture, test_step_size_edge_cases)
+{
+ auto path = createStraightPath(0.0, 0.0, 1.0, 10);
+ auto wall_points = layer_->getWallPoints(path);
+ EXPECT_TRUE(wall_points.size() <= path.poses.size() * 2);
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_malformed_data)
+{
+ // Test with NaN coordinates
+ nav_msgs::msg::Path nan_path;
+ nan_path.header.frame_id = "map";
+ nan_path.header.stamp = node_->now();
+
+ geometry_msgs::msg::PoseStamped nan_pose;
+ nan_pose.header = nan_path.header;
+ nan_pose.pose.position.x = std::numeric_limits::quiet_NaN();
+ nan_pose.pose.position.y = 0.0;
+ nan_pose.pose.orientation.w = 1.0;
+ nan_path.poses.push_back(nan_pose);
+
+ EXPECT_NO_THROW(layer_->getWallPoints(nan_path));
+}
+
+TEST_F(TrackingErrorLayerTestFixture, test_frame_transformation)
+{
+ // Create path in different frame
+ auto path = createStraightPath();
+ path.header.frame_id = "base_link";
+
+ auto tracking_error = createTrackingFeedback(5);
+ layer_->pathCallback(std::make_shared(path));
+ layer_->trackingCallback(std::make_shared(tracking_error));
+
+ nav2_costmap_2d::Costmap2D master_grid(100, 100, 0.05, -2.5, -2.5);
+ // Should handle frame mismatch
+ EXPECT_NO_THROW(layer_->updateCosts(master_grid, 0, 0, 100, 100));
+}
+
+int main(int argc, char ** argv)
+{
+ if (rcutils_logging_set_logger_level("test_tracking_error_layer",
+ RCUTILS_LOG_SEVERITY_ERROR) != RCUTILS_RET_OK)
+ {
+ std::cerr << "Failed to set logger level" << std::endl;
+ }
+ rclcpp::init(argc, argv);
+ testing::InitGoogleTest(&argc, argv);
+ int result = RUN_ALL_TESTS();
+ rclcpp::shutdown();
+ return result;
+}
diff --git a/nav2_util/include/nav2_util/geometry_utils.hpp b/nav2_util/include/nav2_util/geometry_utils.hpp
index f4cf4078042..d12cb06cc49 100644
--- a/nav2_util/include/nav2_util/geometry_utils.hpp
+++ b/nav2_util/include/nav2_util/geometry_utils.hpp
@@ -17,6 +17,7 @@
#include
#include
+#include
#include "geometry_msgs/msg/pose.hpp"
#include "geometry_msgs/msg/pose_stamped.hpp"
@@ -303,6 +304,33 @@ inline double cross_product_2d(
return (path_vec_x * robot_vec_y) - (path_vec_y * robot_vec_x);
}
+inline std::vector> bresenhamLine(int x0, int y0, int x1, int y1)
+{
+ std::vector> cells;
+ int dx = std::abs(x1 - x0);
+ int dy = std::abs(y1 - y0);
+ int sx = (x0 < x1) ? 1 : -1;
+ int sy = (y0 < y1) ? 1 : -1;
+ int err = dx - dy;
+
+ while (true) {
+ cells.push_back({x0, y0});
+ if (x0 == x1 && y0 == y1) {
+ break;
+ }
+ int e2 = 2 * err;
+ if (e2 > -dy) {
+ err -= dy;
+ x0 += sx;
+ }
+ if (e2 < dx) {
+ err += dx;
+ y0 += sy;
+ }
+ }
+ return cells;
+}
+
} // namespace geometry_utils
} // namespace nav2_util