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