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