Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
map_io.cpp
1 /* Copyright 2019 Rover Robotics
2  * Copyright 2010 Brian Gerkey
3  * Copyright (c) 2008, Willow Garage, Inc.
4  *
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions are met:
9  *
10  * * Redistributions of source code must retain the above copyright
11  * notice, this list of conditions and the following disclaimer.
12  * * Redistributions in binary form must reproduce the above copyright
13  * notice, this list of conditions and the following disclaimer in the
14  * documentation and/or other materials provided with the distribution.
15  * * Neither the name of the <ORGANIZATION> nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29  * POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 #include "nav2_map_server/map_io.hpp"
33 #include "nav2_ros_common/validate_messages.hpp"
34 #include "rclcpp/rclcpp.hpp"
35 
36 #ifndef _WIN32
37 #include <libgen.h>
38 #endif
39 
40 #include <Eigen/Dense>
41 
42 #include <iostream>
43 #include <string>
44 #include <vector>
45 #include <fstream>
46 #include <stdexcept>
47 #include <cstdlib>
48 
49 #include "Magick++.h"
50 #include "nav2_util/geometry_utils.hpp"
51 
52 #include "yaml-cpp/yaml.h"
53 
54 #include "tf2/LinearMath/Matrix3x3.hpp"
55 #include "tf2/LinearMath/Quaternion.hpp"
56 #include "nav2_util/occ_grid_values.hpp"
57 
58 #ifdef _WIN32
59 // https://github.com/rtv/Stage/blob/master/replace/dirname.c
60 static
61 char * dirname(char * path)
62 {
63  static const char dot[] = ".";
64  char * last_slash;
65 
66  if (path == NULL) {
67  return path;
68  }
69 
70  /* Replace all "\" with "/" */
71  char * c = path;
72  while (*c != '\0') {
73  if (*c == '\\') {*c = '/';}
74  ++c;
75  }
76 
77  /* Find last '/'. */
78  last_slash = path != NULL ? strrchr(path, '/') : NULL;
79 
80  if (last_slash != NULL && last_slash == path) {
81  /* The last slash is the first character in the string. We have to
82  return "/". */
83  ++last_slash;
84  } else if (last_slash != NULL && last_slash[1] == '\0') {
85  /* The '/' is the last character, we have to look further. */
86  last_slash = reinterpret_cast<char *>(memchr(path, last_slash - path, '/'));
87  }
88 
89  if (last_slash != NULL) {
90  /* Terminate the path. */
91  last_slash[0] = '\0';
92  } else {
93  /* This assignment is ill-designed but the XPG specs require to
94  return a string containing "." in any case no directory part is
95  found and so a static and constant string is required. */
96  path = const_cast<char *>(dot);
97  }
98 
99  return path;
100 }
101 #endif
102 
103 namespace nav2_map_server
104 {
105 using nav2_util::geometry_utils::orientationAroundZAxis;
106 
107 // === Map input part ===
108 
113 template<typename T>
114 T yaml_get_value(const YAML::Node & node, const std::string & key)
115 {
116  try {
117  return node[key].as<T>();
118  } catch (YAML::Exception & e) {
119  std::stringstream ss;
120  ss << "Failed to parse YAML tag '" << key << "' for reason: " << e.msg;
121  throw YAML::Exception(e.mark, ss.str());
122  }
123 }
124 
125 std::string get_home_dir()
126 {
127  if (const char * home_dir = std::getenv("HOME")) {
128  return std::string{home_dir};
129  }
130  return std::string{};
131 }
132 
133 std::string expand_user_home_dir_if_needed(
134  std::string yaml_filename,
135  std::string home_variable_value)
136 {
137  if (yaml_filename.size() < 2 || !(yaml_filename[0] == '~' && yaml_filename[1] == '/')) {
138  return yaml_filename;
139  }
140  if (home_variable_value.empty()) {
141  RCLCPP_INFO_STREAM(
142  rclcpp::get_logger(
143  "map_io"), "Map yaml file name starts with '~/' but no HOME variable set. \n"
144  << "[INFO] [map_io] User home dir will be not expanded \n");
145  return yaml_filename;
146  }
147  const std::string prefix{home_variable_value};
148  return yaml_filename.replace(0, 1, prefix);
149 }
150 
151 LoadParameters loadMapYaml(const std::string & yaml_filename)
152 {
153  YAML::Node doc = YAML::LoadFile(expand_user_home_dir_if_needed(yaml_filename, get_home_dir()));
154  LoadParameters load_parameters;
155 
156  auto image_file_name = yaml_get_value<std::string>(doc, "image");
157  if (image_file_name.empty()) {
158  throw YAML::Exception(doc["image"].Mark(), "The image tag was empty.");
159  }
160  if (image_file_name[0] != '/') {
161  // dirname takes a mutable char *, so we copy into a vector
162  std::vector<char> fname_copy(yaml_filename.begin(), yaml_filename.end());
163  fname_copy.push_back('\0');
164  image_file_name = std::string(dirname(fname_copy.data())) + '/' + image_file_name;
165  }
166  load_parameters.image_file_name = image_file_name;
167 
168  load_parameters.resolution = yaml_get_value<double>(doc, "resolution");
169  load_parameters.origin = yaml_get_value<std::vector<double>>(doc, "origin");
170  if (load_parameters.origin.size() != 3) {
171  throw YAML::Exception(
172  doc["origin"].Mark(), "value of the 'origin' tag should have 3 elements, not " +
173  std::to_string(load_parameters.origin.size()));
174  }
175 
176  load_parameters.free_thresh = yaml_get_value<double>(doc, "free_thresh");
177  load_parameters.occupied_thresh = yaml_get_value<double>(doc, "occupied_thresh");
178 
179  auto map_mode_node = doc["mode"];
180  if (!map_mode_node.IsDefined()) {
181  load_parameters.mode = MapMode::Trinary;
182  } else {
183  load_parameters.mode = map_mode_from_string(map_mode_node.as<std::string>());
184  }
185 
186  try {
187  load_parameters.negate = yaml_get_value<int>(doc, "negate");
188  } catch (YAML::Exception &) {
189  load_parameters.negate = yaml_get_value<bool>(doc, "negate");
190  }
191 
192  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "resolution: " << load_parameters.resolution);
193  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "origin[0]: " << load_parameters.origin[0]);
194  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "origin[1]: " << load_parameters.origin[1]);
195  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "origin[2]: " << load_parameters.origin[2]);
196  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "free_thresh: " << load_parameters.free_thresh);
197  RCLCPP_INFO_STREAM(
198  rclcpp::get_logger(
199  "map_io"), "occupied_thresh: " << load_parameters.occupied_thresh);
200  RCLCPP_INFO_STREAM(
201  rclcpp::get_logger("map_io"),
202  "mode: " << map_mode_to_string(load_parameters.mode));
203  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "negate: " << load_parameters.negate);
204 
205  return load_parameters;
206 }
207 
208 void loadMapFromFile(
209  const LoadParameters & load_parameters,
210  nav_msgs::msg::OccupancyGrid & map)
211 {
212  Magick::InitializeMagick(nullptr);
213  nav_msgs::msg::OccupancyGrid msg;
214 
215  RCLCPP_INFO_STREAM(
216  rclcpp::get_logger("map_io"), "Loading image_file: " <<
217  load_parameters.image_file_name);
218  Magick::Image img(load_parameters.image_file_name);
219 
220  // Copy the image data into the map structure
221  msg.info.width = img.size().width();
222  msg.info.height = img.size().height();
223 
224  msg.info.resolution = load_parameters.resolution;
225  msg.info.origin.position.x = load_parameters.origin[0];
226  msg.info.origin.position.y = load_parameters.origin[1];
227  msg.info.origin.position.z = 0.0;
228  msg.info.origin.orientation = orientationAroundZAxis(load_parameters.origin[2]);
229 
230  // Allocate space to hold the data
231  msg.data.resize(msg.info.width * msg.info.height);
232 
233  // Convert the image to grayscale
234  Magick::Image gray = img;
235  gray.type(Magick::GrayscaleType);
236 
237  // Prepare grayscale matrix from image
238  size_t width = gray.columns();
239  size_t height = gray.rows();
240 
241  std::vector<uint8_t> buffer(width * height);
242  gray.write(0, 0, width, height, "I", Magick::CharPixel, buffer.data());
243 
244  // Map the grayscale buffer to an Eigen matrix (row-major layout)
245  Eigen::Map<Eigen::Matrix<uint8_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>
246  gray_matrix(buffer.data(), height, width);
247 
248  bool has_alpha = img.matte();
249 
250  // Handle different map modes with if else condition
251  // Trinary and Scale modes are handled together
252  // because they share a lot of code
253  // Raw mode is handled separately in else if block
254  Eigen::Matrix<int8_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> result(height, width);
255 
256  if (load_parameters.mode == MapMode::Trinary || load_parameters.mode == MapMode::Scale) {
257  // Convert grayscale to float in range [0.0, 1.0]
258  Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic,
259  Eigen::RowMajor> normalized = gray_matrix.cast<float>() / 255.0f;
260 
261  // Negate the image if specified (e.g. for black=occupied vs. white=occupied convention)
262  if (!load_parameters.negate) {
263  normalized = (1.0f - normalized.array()).matrix();
264  }
265 
266  // Compute binary occupancy masks
267  Eigen::Matrix<uint8_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> occupied =
268  (normalized.array() >= load_parameters.occupied_thresh).cast<uint8_t>();
269 
270  Eigen::Matrix<uint8_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> free =
271  (normalized.array() <= load_parameters.free_thresh).cast<uint8_t>();
272 
273  // Initialize occupancy grid with UNKNOWN values (-1)
274  result.setConstant(nav2_util::OCC_GRID_UNKNOWN);
275 
276  // Apply occupied and free cell updates
277  result = (occupied.array() > 0).select(nav2_util::OCC_GRID_OCCUPIED, result);
278  result = (free.array() > 0).select(nav2_util::OCC_GRID_FREE, result);
279 
280  // Handle intermediate (gray) values if in Scale mode
281  if (load_parameters.mode == MapMode::Scale) {
282  // Create in-between mask
283  auto in_between_mask = (normalized.array() > load_parameters.free_thresh) &&
284  (normalized.array() < load_parameters.occupied_thresh);
285 
286  if (in_between_mask.any()) {
287  // Scale in-between values to [0,100] range
288  Eigen::ArrayXXf scaled_float = ((normalized.array() - load_parameters.free_thresh) /
289  (load_parameters.occupied_thresh - load_parameters.free_thresh)) * 100.0f;
290 
291  // Round and cast to int8_t
292  Eigen::Matrix<int8_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> scaled_int =
293  scaled_float.array().round().cast<int8_t>();
294 
295  result = in_between_mask.select(scaled_int, result);
296  }
297  }
298 
299  // Apply alpha transparency mask: mark transparent cells as UNKNOWN
300  if (has_alpha) {
301  // Allocate buffer only once and map directly to Eigen without extra copy
302  std::vector<uint8_t> alpha_buf(width * height);
303  img.write(0, 0, width, height, "A", Magick::CharPixel, alpha_buf.data());
304 
305  // Map alpha buffer as Eigen::Array for efficient elementwise ops
306  Eigen::Map<Eigen::Array<uint8_t, Eigen::Dynamic, Eigen::Dynamic,
307  Eigen::RowMajor>> alpha_array(
308  alpha_buf.data(), height, width);
309 
310  // Apply mask directly with Eigen::select
311  result = (alpha_array < 255).select(nav2_util::OCC_GRID_UNKNOWN, result);
312  }
313 
314  } else if (load_parameters.mode == MapMode::Raw) {
315  // Raw mode: interpret raw image pixel values directly as occupancy values
316  result = gray_matrix.cast<int8_t>();
317 
318  // Clamp out-of-bound values (outside [-1, 100]) to UNKNOWN (-1)
319  auto out_of_bounds = (result.array() < nav2_util::OCC_GRID_FREE) ||
320  (result.array() > nav2_util::OCC_GRID_OCCUPIED);
321 
322  result = out_of_bounds.select(nav2_util::OCC_GRID_UNKNOWN, result);
323 
324  } else {
325  // If the map mode is not recognized, throw an error
326  throw std::runtime_error("Invalid map mode");
327  }
328 
329  // Flip image vertically (as ROS expects origin at bottom-left)
330  Eigen::Matrix<int8_t, Eigen::Dynamic, Eigen::Dynamic,
331  Eigen::RowMajor> flipped = result.colwise().reverse();
332  std::memcpy(msg.data.data(), flipped.data(), width * height);
333 
334  // Since loadMapFromFile() does not belong to any node, publishing in a system time.
335  rclcpp::Clock clock(RCL_SYSTEM_TIME);
336  msg.info.map_load_time = clock.now();
337  msg.header.frame_id = "map";
338  msg.header.stamp = clock.now();
339 
340  RCLCPP_INFO_STREAM(
341  rclcpp::get_logger(
342  "map_io"), "Read map " << load_parameters.image_file_name
343  << ": " << msg.info.width << " X " << msg.info.height << " map @ "
344  << msg.info.resolution << " m/cell");
345 
346  map = msg;
347 }
348 
349 LOAD_MAP_STATUS loadMapFromYaml(
350  const std::string & yaml_file,
351  nav_msgs::msg::OccupancyGrid & map)
352 {
353  if (yaml_file.empty()) {
354  RCLCPP_ERROR_STREAM(rclcpp::get_logger("map_io"), "YAML file name is empty, can't load!");
355  return MAP_DOES_NOT_EXIST;
356  }
357  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "Loading yaml file: " << yaml_file);
358  LoadParameters load_parameters;
359  try {
360  load_parameters = loadMapYaml(yaml_file);
361  } catch (YAML::Exception & e) {
362  RCLCPP_ERROR_STREAM(
363  rclcpp::get_logger(
364  "map_io"), "Failed processing YAML file " << yaml_file << " at position (" <<
365  e.mark.line << ":" << e.mark.column << ") for reason: " << e.what());
366  return INVALID_MAP_METADATA;
367  } catch (std::exception & e) {
368  RCLCPP_ERROR_STREAM(
369  rclcpp::get_logger("map_io"), "Failed to parse map YAML loaded from file " << yaml_file <<
370  " for reason: " << e.what());
371  return INVALID_MAP_METADATA;
372  }
373  try {
374  loadMapFromFile(load_parameters, map);
375  } catch (std::exception & e) {
376  RCLCPP_ERROR_STREAM(
377  rclcpp::get_logger(
378  "map_io"), "Failed to load image file " << load_parameters.image_file_name <<
379  " for reason: " << e.what());
380  return INVALID_MAP_DATA;
381  }
382 
383  return LOAD_MAP_SUCCESS;
384 }
385 
386 // === Map output part ===
387 
394 void checkSaveParameters(SaveParameters & save_parameters)
395 {
396  // Magick must me initialized before any activity with images
397  Magick::InitializeMagick(nullptr);
398 
399  // Checking map file name
400  if (save_parameters.map_file_name == "") {
401  rclcpp::Clock clock(RCL_SYSTEM_TIME);
402  save_parameters.map_file_name = "map_" +
403  std::to_string(static_cast<int>(clock.now().seconds()));
404  RCLCPP_WARN_STREAM(
405  rclcpp::get_logger("map_io"), "Map file unspecified. Map will be saved to " <<
406  save_parameters.map_file_name << " file");
407  }
408 
409  // Checking thresholds
410  if (save_parameters.occupied_thresh == 0.0) {
411  save_parameters.occupied_thresh = 0.65;
412  RCLCPP_WARN_STREAM(
413  rclcpp::get_logger(
414  "map_io"), "Occupied threshold unspecified. Setting it to default value: " <<
415  save_parameters.occupied_thresh);
416  }
417  if (save_parameters.free_thresh == 0.0) {
418  save_parameters.free_thresh = 0.25;
419  RCLCPP_WARN_STREAM(
420  rclcpp::get_logger("map_io"), "Free threshold unspecified. Setting it to default value: " <<
421  save_parameters.free_thresh);
422  }
423  if (1.0 < save_parameters.occupied_thresh) {
424  RCLCPP_ERROR_STREAM(rclcpp::get_logger("map_io"), "Threshold_occupied must be 1.0 or less");
425  throw std::runtime_error("Incorrect thresholds");
426  }
427  if (save_parameters.free_thresh < 0.0) {
428  RCLCPP_ERROR_STREAM(rclcpp::get_logger("map_io"), "Free threshold must be 0.0 or greater");
429  throw std::runtime_error("Incorrect thresholds");
430  }
431  if (save_parameters.occupied_thresh <= save_parameters.free_thresh) {
432  RCLCPP_ERROR_STREAM(
433  rclcpp::get_logger(
434  "map_io"), "Threshold_free must be smaller than threshold_occupied");
435  throw std::runtime_error("Incorrect thresholds");
436  }
437 
438  // Checking image format
439  if (save_parameters.image_format == "") {
440  save_parameters.image_format = save_parameters.mode == MapMode::Scale ? "png" : "pgm";
441  RCLCPP_WARN_STREAM(
442  rclcpp::get_logger("map_io"), "Image format unspecified. Setting it to: " <<
443  save_parameters.image_format);
444  }
445 
446  std::transform(
447  save_parameters.image_format.begin(),
448  save_parameters.image_format.end(),
449  save_parameters.image_format.begin(),
450  [](unsigned char c) {return std::tolower(c);});
451 
452  const std::vector<std::string> BLESSED_FORMATS{"bmp", "pgm", "png"};
453  if (
454  std::find(BLESSED_FORMATS.begin(), BLESSED_FORMATS.end(), save_parameters.image_format) ==
455  BLESSED_FORMATS.end())
456  {
457  std::stringstream ss;
458  bool first = true;
459  for (auto & format_name : BLESSED_FORMATS) {
460  if (!first) {
461  ss << ", ";
462  }
463  ss << "'" << format_name << "'";
464  first = false;
465  }
466  RCLCPP_WARN_STREAM(
467  rclcpp::get_logger("map_io"), "Requested image format '" << save_parameters.image_format <<
468  "' is not one of the recommended formats: " << ss.str());
469  }
470  const std::string FALLBACK_FORMAT = "png";
471 
472  try {
473  Magick::CoderInfo info(save_parameters.image_format);
474  if (!info.isWritable()) {
475  RCLCPP_WARN_STREAM(
476  rclcpp::get_logger("map_io"), "Format '" << save_parameters.image_format <<
477  "' is not writable. Using '" << FALLBACK_FORMAT << "' instead");
478  save_parameters.image_format = FALLBACK_FORMAT;
479  }
480  } catch (Magick::ErrorOption & e) {
481  RCLCPP_WARN_STREAM(
482  rclcpp::get_logger(
483  "map_io"), "Format '" << save_parameters.image_format << "' is not usable. Using '" <<
484  FALLBACK_FORMAT << "' instead:" << std::endl << e.what());
485  save_parameters.image_format = FALLBACK_FORMAT;
486  }
487 
488  // Checking map mode
489  if (
490  save_parameters.mode == MapMode::Scale &&
491  (save_parameters.image_format == "pgm" ||
492  save_parameters.image_format == "jpg" ||
493  save_parameters.image_format == "jpeg"))
494  {
495  RCLCPP_WARN_STREAM(
496  rclcpp::get_logger("map_io"), "Map mode 'scale' requires transparency, but format '" <<
497  save_parameters.image_format <<
498  "' does not support it. Consider switching image format to 'png'.");
499  }
500 }
501 
508 void tryWriteMapToFile(
509  const nav_msgs::msg::OccupancyGrid & map,
510  const SaveParameters & save_parameters)
511 {
512  RCLCPP_INFO_STREAM(
513  rclcpp::get_logger(
514  "map_io"), "Received a " << map.info.width << " X " << map.info.height << " map @ " <<
515  map.info.resolution << " m/pix");
516 
517  std::string mapdatafile = save_parameters.map_file_name + "." + save_parameters.image_format;
518  {
519  // should never see this color, so the initialization value is just for debugging
520  Magick::Image image({map.info.width, map.info.height}, "red");
521 
522  // In scale mode, we need the alpha (matte) channel. Else, we don't.
523  // NOTE: GraphicsMagick seems to have trouble loading the alpha channel when saved with
524  // Magick::GreyscaleMatte, so we use TrueColorMatte instead.
525  image.type(
526  save_parameters.mode == MapMode::Scale ?
527  Magick::TrueColorMatteType : Magick::GrayscaleType);
528 
529  // Since we only need to support 100 different pixel levels, 8 bits is fine
530  image.depth(8);
531 
532  int free_thresh_int = std::rint(save_parameters.free_thresh * 100.0);
533  int occupied_thresh_int = std::rint(save_parameters.occupied_thresh * 100.0);
534 
535  for (size_t y = 0; y < map.info.height; y++) {
536  for (size_t x = 0; x < map.info.width; x++) {
537  int8_t map_cell = map.data[map.info.width * (map.info.height - y - 1) + x];
538 
539  Magick::Color pixel;
540 
541  switch (save_parameters.mode) {
542  case MapMode::Trinary:
543  if (map_cell < 0 || 100 < map_cell) {
544  pixel = Magick::ColorGray(205 / 255.0);
545  } else if (map_cell <= free_thresh_int) {
546  pixel = Magick::ColorGray(254 / 255.0);
547  } else if (occupied_thresh_int <= map_cell) {
548  pixel = Magick::ColorGray(0 / 255.0);
549  } else {
550  pixel = Magick::ColorGray(205 / 255.0);
551  }
552  break;
553  case MapMode::Scale:
554  if (map_cell < 0 || 100 < map_cell) {
555  pixel = Magick::ColorGray{0.5};
556  pixel.alphaQuantum(TransparentOpacity);
557  } else {
558  pixel = Magick::ColorGray{(100.0 - map_cell) / 100.0};
559  }
560  break;
561  case MapMode::Raw:
562  Magick::Quantum q;
563  if (map_cell < 0 || 100 < map_cell) {
564  q = MaxRGB;
565  } else {
566  q = map_cell / 255.0 * MaxRGB;
567  }
568  pixel = Magick::Color(q, q, q);
569  break;
570  default:
571  RCLCPP_ERROR_STREAM(
572  rclcpp::get_logger(
573  "map_io"), "Map mode should be Trinary, Scale or Raw");
574  throw std::runtime_error("Invalid map mode");
575  }
576  image.pixelColor(x, y, pixel);
577  }
578  }
579 
580  RCLCPP_INFO_STREAM(
581  rclcpp::get_logger("map_io"),
582  "Writing map occupancy data to " << mapdatafile);
583  image.write(mapdatafile);
584  }
585 
586  std::string mapmetadatafile = save_parameters.map_file_name + ".yaml";
587  {
588  std::ofstream yaml(mapmetadatafile);
589 
590  geometry_msgs::msg::Quaternion orientation = map.info.origin.orientation;
591  tf2::Matrix3x3 mat(tf2::Quaternion(orientation.x, orientation.y, orientation.z, orientation.w));
592  double yaw, pitch, roll;
593  mat.getEulerYPR(yaw, pitch, roll);
594 
595  const int file_name_index = mapdatafile.find_last_of("/\\");
596  std::string image_name = mapdatafile.substr(file_name_index + 1);
597 
598  YAML::Emitter e;
599  e << YAML::Precision(7);
600  e << YAML::BeginMap;
601  e << YAML::Key << "image" << YAML::Value << image_name;
602  e << YAML::Key << "mode" << YAML::Value << map_mode_to_string(save_parameters.mode);
603  e << YAML::Key << "resolution" << YAML::Value << to_string_with_precision(
604  map.info.resolution,
605  3);
606  e << YAML::Key << "origin" << YAML::Flow << YAML::BeginSeq <<
607  to_string_with_precision(map.info.origin.position.x, 3) <<
608  to_string_with_precision(map.info.origin.position.y, 3) << yaw << YAML::EndSeq;
609  e << YAML::Key << "negate" << YAML::Value << 0;
610 
611  if (save_parameters.mode == MapMode::Trinary) {
612  // For Trinary mode, the thresholds depend on the pixel values in the saved map,
613  // not on the thresholds used to threshold the map.
614  // As these values are fixed above, the thresholds must also be fixed to separate the
615  // pixel values into occupied, free and unknown.
616  e << YAML::Key << "occupied_thresh" << YAML::Value << 0.65;
617  e << YAML::Key << "free_thresh" << YAML::Value << 0.196;
618  } else {
619  e << YAML::Key << "occupied_thresh" << YAML::Value <<
620  to_string_with_precision(save_parameters.occupied_thresh, 3);
621  e << YAML::Key << "free_thresh" << YAML::Value <<
622  to_string_with_precision(save_parameters.free_thresh, 3);
623  }
624 
625  if (!e.good()) {
626  RCLCPP_ERROR_STREAM(
627  rclcpp::get_logger("map_io"), "YAML writer failed with an error " << e.GetLastError() <<
628  ". The map metadata may be invalid.");
629  }
630 
631  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "Writing map metadata to " << mapmetadatafile);
632  std::ofstream(mapmetadatafile) << e.c_str();
633  }
634  RCLCPP_INFO_STREAM(rclcpp::get_logger("map_io"), "Map saved");
635 }
636 
637 bool saveMapToFile(
638  const nav_msgs::msg::OccupancyGrid & map,
639  const SaveParameters & save_parameters)
640 {
641  if (!nav2::validateMsg(map)) {
642  RCLCPP_ERROR_STREAM(
643  rclcpp::get_logger("map_io"),
644  "Failed to write map for reason: invalid OccupancyGrid message");
645  return false;
646  }
647 
648  // Local copy of SaveParameters that might be modified by checkSaveParameters()
649  SaveParameters save_parameters_loc = save_parameters;
650 
651  try {
652  // Checking map parameters for consistency
653  checkSaveParameters(save_parameters_loc);
654 
655  tryWriteMapToFile(map, save_parameters_loc);
656  } catch (std::exception & e) {
657  RCLCPP_ERROR_STREAM(
658  rclcpp::get_logger("map_io"),
659  "Failed to write map for reason: " << e.what());
660  return false;
661  }
662  return true;
663 }
664 
665 std::string to_string_with_precision(double value, int precision)
666 {
667  std::ostringstream out;
668  out << std::fixed << std::setprecision(precision) << value;
669 
670  return out.str();
671 }
672 
673 } // namespace nav2_map_server