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