5#include <GeographicLib/Geodesic.hpp>
39 const std::string& description,
40 const bool add_to_auto_reconfigurable_params,
41 const bool is_required,
43 const std::optional<double>& from_value,
44 const std::optional<double>& to_value,
45 const std::optional<double>& step_value,
46 const std::string& additional_constraints) {
47 rcl_interfaces::msg::ParameterDescriptor param_desc;
48 param_desc.description = description;
49 param_desc.additional_constraints = additional_constraints;
50 param_desc.read_only = read_only;
52 auto type = rclcpp::ParameterValue(param).get_type();
54 if (from_value.has_value() && to_value.has_value()) {
55 if constexpr (std::is_integral_v<T>) {
56 rcl_interfaces::msg::IntegerRange range;
57 range.set__from_value(
static_cast<T
>(from_value.value())).set__to_value(
static_cast<T
>(to_value.value()));
58 if (step_value.has_value()) range.set__step(
static_cast<T
>(step_value.value()));
59 param_desc.integer_range = {range};
60 }
else if constexpr (std::is_floating_point_v<T>) {
61 rcl_interfaces::msg::FloatingPointRange range;
62 range.set__from_value(
static_cast<T
>(from_value.value())).set__to_value(
static_cast<T
>(to_value.value()));
63 if (step_value.has_value()) range.set__step(
static_cast<T
>(step_value.value()));
64 param_desc.floating_point_range = {range};
66 RCLCPP_WARN(this->get_logger(),
"Parameter type of parameter '%s' does not support specifying a range", name.c_str());
70 this->declare_parameter(name, type, param_desc);
71 bool initial_set =
false;
74 param = this->get_parameter(name).get_value<T>();
76 ss <<
"Loaded parameter '" << name <<
"': ";
79 for (
const auto& element : param) ss << element << (&element != ¶m.back() ?
", " :
"");
84 RCLCPP_INFO_STREAM(this->get_logger(), ss.str());
86 }
catch (rclcpp::exceptions::ParameterUninitializedException&) {
88 RCLCPP_FATAL_STREAM(this->get_logger(),
"Missing required parameter '" << name <<
"', exiting");
92 ss <<
"Missing parameter '" << name <<
"', using default value: ";
95 for (
const auto& element : param) ss << element << (&element != ¶m.back() ?
", " :
"");
100 RCLCPP_WARN_STREAM(this->get_logger(), ss.str());
101 this->set_parameters({rclcpp::Parameter(name, rclcpp::ParameterValue(param))});
105 if (add_to_auto_reconfigurable_params) {
106 std::function<void(
const rclcpp::Parameter&)> setter = [¶m](
const rclcpp::Parameter& p) { param = p.get_value<T>(); };
115 rcl_interfaces::msg::SetParametersResult result;
116 result.successful =
false;
119 bool relevant_change =
false;
121 bool map_filepath_changed =
false;
122 bool origin_lat_changed =
false;
123 bool origin_lon_changed =
false;
125 RCLCPP_INFO(this->get_logger(),
"Received request to change %zu parameter(s)", parameters.size());
126 for (
const auto& p : parameters) {
127 if (p.get_name() ==
"map_contents") {
128 RCLCPP_INFO(this->get_logger(),
"Incoming param: map_contents = <omitted, size=%zu>", p.get_value<std::string>().size());
130 RCLCPP_INFO(this->get_logger(),
"Incoming param: %s = %s", p.get_name().c_str(), p.value_to_string().c_str());
134 for (
const auto& param : parameters) {
136 if (param.get_name() == std::get<0>(auto_reconfigurable_param)) {
137 std::get<1>(auto_reconfigurable_param)(param);
138 RCLCPP_INFO(this->get_logger(),
"Reconfigured parameter '%s' to: %s", param.get_name().c_str(),
139 param.value_to_string().c_str());
144 if (param.get_name() ==
"map_filepath") {
146 result.reason =
"Parameter 'map_filepath' is not reconfigurable when automatic map selection is enabled.";
149 map_filepath_changed = previous_map_filepath !=
map_filepath_;
150 relevant_change = relevant_change || map_filepath_changed;
152 if (param.get_name() ==
"origin_lat") {
154 result.reason =
"Parameter 'origin_lat' is not reconfigurable when automatic map selection is enabled.";
157 origin_lat_changed = previous_origin_lat !=
origin_lat_;
158 relevant_change = relevant_change || origin_lat_changed;
162 if (param.get_name() ==
"origin_lon") {
164 result.reason =
"Parameter 'origin_lon' is not reconfigurable when automatic map selection is enabled.";
167 origin_lon_changed = previous_origin_lon !=
origin_lon_;
168 relevant_change = relevant_change || origin_lon_changed;
172 if (param.get_name() ==
"map_contents" && param.get_value<std::string>() !=
map_contents_) {
173 result.reason =
"Parameter 'map_contents' is not reconfigurable.";
179 RCLCPP_ERROR(this->get_logger(),
"No Lanelet2 maps found in '%s'",
map_directory_.c_str());
181 one_shot_timer_ = this->create_wall_timer(std::chrono::milliseconds(1), [
this]() {
193 result.reason =
"Could not derive lower-left origin from map in manual mode.";
194 result.successful =
false;
198 RCLCPP_INFO(this->get_logger(),
"Derived manual origin from lower-left map corner for '%s' (lat=%.9f, lon=%.9f)",
206 one_shot_timer_ = this->create_wall_timer(std::chrono::milliseconds(1), [
this]() {
215 RCLCPP_ERROR(this->get_logger(),
"Map sanity check failed for map '%s' with origin (lat=%.9f, lon=%.9f)",
217 result.reason =
"Map sanity check failed in manual mode.";
218 result.successful =
false;
223 RCLCPP_WARN(this->get_logger(),
224 "Waiting for origin readiness: if origin is explictly set, both parameters must be provided.");
225 result.reason =
"Waiting for origin readiness: if origin is explictly set, both parameters must be provided.";
226 result.successful =
true;
230 result.successful =
true;
244 RCLCPP_ERROR(this->get_logger(),
"No Lanelet2 maps found in '%s'",
map_directory_.c_str());
255 RCLCPP_ERROR(this->get_logger(),
"Could not derive lower-left origin from map '%s' in manual mode.",
260 RCLCPP_INFO(this->get_logger(),
"Derived manual origin from lower-left map corner for '%s' (lat=%.9f, lon=%.9f)",
263 RCLCPP_ERROR(this->get_logger(),
264 "Manual mode requires both 'origin_lat' and 'origin_lon' when either is provided. Received origin_lat set=%s, "
265 "origin_lon set=%s.",
276 RCLCPP_ERROR(this->get_logger(),
"Map sanity check failed for map '%s' with origin (lat=%.9f, lon=%.9f)",
284 if (map_filepath.empty()) {
285 RCLCPP_ERROR(this->get_logger(),
"No map filepath provided, cannot derive origin from map.");
304 for (
const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
305 if (!entry.is_regular_file()) {
308 if (entry.path().extension() ==
".osm") {
310 map_meta.
map_path = entry.path().string();
311 maps.emplace_back(map_meta);
314 }
catch (
const std::filesystem::filesystem_error& e) {
315 RCLCPP_ERROR_STREAM(get_logger(),
"Failed to scan '" << directory <<
"': " << e.what());
317 for (
auto& map_meta : maps) {
320 RCLCPP_INFO(this->get_logger(),
"Discovered %zu Lanelet2 maps in '%s'", maps.size(), directory.c_str());
322 RCLCPP_INFO(this->get_logger(),
"Map '%s': min_lat=%.9f, min_lon=%.9f, max_lat=%.9f, max_lon=%.9f, diagonal_length=%.3f",
323 map_meta.map_path.c_str(), map_meta.min_lat, map_meta.min_lon, map_meta.max_lat, map_meta.max_lon,
324 map_meta.diagonal_length);
330 pugi::xml_document document;
331 const pugi::xml_parse_result result = document.load_file(map_meta.
map_path.c_str());
333 RCLCPP_ERROR_STREAM(get_logger(),
"Unable to parse map '" << map_meta.
map_path <<
"': " << result.description());
337 pugi::xml_node root = document.child(
"osm");
339 root = document.document_element();
342 RCLCPP_WARN_STREAM(get_logger(),
"No root element found when deriving meta for '" << map_meta.
map_path <<
"'");
346 const double kDoubleMax = std::numeric_limits<double>::max();
347 const double kDoubleLowest = std::numeric_limits<double>::lowest();
348 double min_lat = kDoubleMax;
349 double min_lon = kDoubleMax;
350 double max_lat = kDoubleLowest;
351 double max_lon = kDoubleLowest;
352 bool found_node =
false;
354 for (pugi::xml_node node : root.children(
"node")) {
355 const double lat = node.attribute(
"lat").as_double(std::numeric_limits<double>::quiet_NaN());
356 const double lon = node.attribute(
"lon").as_double(std::numeric_limits<double>::quiet_NaN());
357 if (std::isnan(lat) || std::isnan(lon)) {
361 min_lat = std::min(min_lat, lat);
362 max_lat = std::max(max_lat, lat);
363 min_lon = std::min(min_lon, lon);
364 max_lon = std::max(max_lon, lon);
369 RCLCPP_WARN_STREAM(get_logger(),
"No node elements found while deriving bounds for '" << map_meta.
map_path <<
"'");
378 double distance = 0.0;
379 GeographicLib::Geodesic::WGS84().Inverse(min_lat, min_lon, max_lat, max_lon, distance);
391 RCLCPP_WARN(this->get_logger(),
"No initial GPS fix received, unable to select map");
399 if (map_meta.diagonal_length <= 0.0) {
408 if (
map_sanity_check(map_meta.map_path, map_meta.min_lat, map_meta.min_lon)) {
409 RCLCPP_DEBUG(this->get_logger(),
"Map sanity check passed for '%s'", map_meta.map_path.c_str());
410 selected_map = &map_meta;
412 RCLCPP_WARN(this->get_logger(),
"Map sanity check failed for '%s'", map_meta.map_path.c_str());
418 if (selected_map ==
nullptr) {
419 RCLCPP_WARN(this->get_logger(),
"No suitable map found for current GPS location (lat=%.9f, lon=%.9f)",
current_latitude_,
426 RCLCPP_DEBUG(this->get_logger(),
"Currently loaded map '%s' is still valid",
map_filepath_.c_str());
446 map_contents_ = std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
447 RCLCPP_INFO(this->get_logger(),
"Loaded map contents from '%s' to parameter 'map_contents'",
map_filepath_.c_str());
451 this->set_parameter(rclcpp::Parameter(
"map_filepath",
map_filepath_));
452 this->set_parameter(rclcpp::Parameter(
"map_contents",
map_contents_));
453 this->set_parameter(rclcpp::Parameter(
"origin_lat",
origin_lat_));
454 this->set_parameter(rclcpp::Parameter(
"origin_lon",
origin_lon_));
468 lanelet::projection::UtmProjector proj(lanelet::Origin({origin_lat, origin_lon}));
470 lanelet::LaneletMapPtr map = lanelet::load(map_filepath, proj);
471 }
catch (
const lanelet::IOError& e) {
472 RCLCPP_ERROR_STREAM(this->get_logger(),
"Could not load map '" << map_filepath <<
"': " << e.what());
480 northp = latitude >= 0.0;
481 zone =
static_cast<int>(std::floor((longitude + 180.0) / 6.0)) + 1;
488 geometry_msgs::msg::TransformStamped t;
494 t.transform.translation.x = origin_utm.x();
495 t.transform.translation.y = origin_utm.y();
496 t.transform.translation.z = origin_utm.z();
498 t.transform.rotation.x = 0;
499 t.transform.rotation.y = 0;
500 t.transform.rotation.z = 0;
501 t.transform.rotation.w = 1;
507 std::string hemisphere;
514 t.header.frame_id =
"utm_" + std::to_string(zone) + hemisphere;
516 t.header.stamp = now();
520 RCLCPP_INFO(get_logger(),
"Broadcast static transform from '%s' to '%s'", t.header.frame_id.c_str(), t.child_frame_id.c_str());
530int main(
int argc,
char** argv) {
531 rclcpp::init(argc, argv);
532 rclcpp::spin(std::make_shared<Lanelet2MapServer>());
void unsetMapParameters()
Clears map-specific parameters after an invalid or unloaded map state.
std::vector< Lanelet2MapMeta > available_maps_
void updateMapParameters()
Writes the current map state into this node's exported ROS parameters.
double current_longitude_
rclcpp::TimerBase::SharedPtr one_shot_timer_
rclcpp::TimerBase::SharedPtr automatic_map_timer_
bool declareAndLoadParameter(const std::string &name, T ¶m, const std::string &description, const bool add_to_auto_reconfigurable_params=true, const bool is_required=false, const bool read_only=false, const std::optional< double > &from_value=std::nullopt, const std::optional< double > &to_value=std::nullopt, const std::optional< double > &step_value=std::nullopt, const std::string &additional_constraints="")
Declares and loads a ROS parameter.
OnSetParametersCallbackHandle::SharedPtr parameters_callback_
Callback handle for dynamic parameter reconfiguration.
void setup()
Sets up timers, callbacks, publishers, and initial map loading.
void automaticMapUpdateTimerCallback()
Periodically reevaluates the best map when automatic selection is enabled.
std::shared_ptr< tf2_ros::StaticTransformBroadcaster > tf_static_broadcaster_
std::string map_filepath_
bool map_sanity_check(std::string map_filepath, double origin_lat, double origin_lon) const
Tries to load a map with the provided origin to verify that it is usable.
void derive_map_meta(Lanelet2MapMeta &map_meta) const
Fills the derived metadata fields for a discovered map.
Lanelet2MapServer()
Creates the Lanelet2 map server node and declares its parameters.
bool deriveOriginFromMap(const std::string &map_filepath, double &origin_lat, double &origin_lon) const
Derives a south-west origin latitude and longitude from a Lanelet2 map file.
std::string map_contents_
rcl_interfaces::msg::SetParametersResult parametersCallback(const std::vector< rclcpp::Parameter > ¶meters)
Handles reconfiguration when a parameter value is changed.
void loadMapContents()
Loads the configured map file and caches its serialized contents.
std::string map_directory_
static void derive_utm_zone(const double latitude, const double longitude, int &zone, bool &northp)
Derives the UTM zone and hemisphere from a geographic position.
void navSatFixCallback(const sensor_msgs::msg::NavSatFix::SharedPtr msg)
Stores the latest GNSS fix for automatic map selection.
std::vector< std::tuple< std::string, std::function< void(const rclcpp::Parameter &)> > > auto_reconfigurable_params_
Auto-reconfigurable parameters for dynamic reconfiguration.
void find_available_maps(const std::string &directory, std::vector< Lanelet2MapMeta > &maps) const
Recursively scans a directory for available Lanelet2 map files.
void pub_tf() const
Publishes the static transform from the derived UTM frame to the map frame.
bool use_automatic_map_selection_
rclcpp::Subscription< sensor_msgs::msg::NavSatFix >::SharedPtr navsat_subscription_
std::string map_frame_id_
int main(int argc, char **argv)
Starts the Lanelet2 map server node.
constexpr bool is_vector_v