Skip to content

Commit 4ec7ff0

Browse files
committed
feat: add autoware_lanelet2_map_merger package
Signed-off-by: Ryohsuke Mitsudome <ryohsuke.mitsudome@tier4.jp>
1 parent 700c544 commit 4ec7ff0

12 files changed

Lines changed: 831 additions & 0 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
cmake_minimum_required(VERSION 3.14)
2+
project(autoware_lanelet2_map_merger)
3+
4+
find_package(autoware_cmake REQUIRED)
5+
autoware_package()
6+
ament_auto_find_build_dependencies()
7+
8+
ament_auto_add_library(${PROJECT_NAME} SHARED
9+
src/lanelet2_map_merger.cpp
10+
src/lanelet2_map_merger_node.cpp
11+
)
12+
13+
rclcpp_components_register_node(${PROJECT_NAME}
14+
PLUGIN "autoware::lanelet2_map_merger::Lanelet2MapMergerNode"
15+
EXECUTABLE ${PROJECT_NAME}_node
16+
)
17+
18+
if(BUILD_TESTING)
19+
find_package(ament_lint_auto REQUIRED)
20+
ament_lint_auto_find_test_dependencies()
21+
22+
ament_add_gtest(test_lanelet2_map_merger test/test_lanelet2_map_merger.cpp)
23+
target_link_libraries(test_lanelet2_map_merger ${PROJECT_NAME})
24+
endif()
25+
26+
ament_auto_package(INSTALL_TO_SHARE launch config)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# autoware_lanelet2_map_merger
2+
3+
This is a tool for processing Lanelet2 (`.osm`) map files. It can perform the following
4+
function:
5+
6+
- Merging multiple divided `.osm` files into a single `.osm` file
7+
8+
It is the inverse of
9+
[`autoware_lanelet2_map_divider`](../autoware_lanelet2_map_divider/README.md).
10+
11+
## Supported Data Format
12+
13+
- Input: a directory containing `.osm` files (typically the `lanelet2_map.osm/`
14+
directory produced by `autoware_lanelet2_map_divider`).
15+
- Output: a single merged `.osm` file.
16+
17+
The projection required to load/save the maps is obtained from a
18+
`map_projector_info.yaml` file (identical to the one consumed by
19+
`autoware_map_projection_loader`). `MGRS`, `LocalCartesianUTM`, `LocalCartesian`,
20+
and `TransverseMercator` are supported.
21+
22+
## Usage
23+
24+
```bash
25+
ros2 launch autoware_lanelet2_map_merger lanelet2_map_merger.launch.xml \
26+
input_lanelet2_map_dir:=<INPUT_DIR> \
27+
output_lanelet2_map:=<OUTPUT_OSM> \
28+
map_projector_info_path:=<PROJECTOR_YAML>
29+
```
30+
31+
| Name | Description |
32+
| --------------------- | ----------------------------------------------------------------- |
33+
| INPUT_DIR | Directory containing the input `.osm` files |
34+
| OUTPUT_OSM | Path of the output merged `.osm` file |
35+
| PROJECTOR_YAML | Path to `map_projector_info.yaml` |
36+
37+
`INPUT_DIR`, `OUTPUT_OSM`, and `PROJECTOR_YAML` should be specified as **absolute paths**.
38+
39+
## Parameters
40+
41+
{{ json_to_markdown("map/autoware_lanelet2_map_merger/schema/lanelet2_map_merger.schema.json") }}
42+
43+
## How the maps are merged
44+
45+
1. Discover every `.osm` file inside the input directory.
46+
2. Load each map using the projector from `map_projector_info.yaml`.
47+
3. Merge all loaded maps into a single `LaneletMap` using the same logic as
48+
`lanelet2_map_loader` (`merge_lanelet2_maps`): lanelets, areas, regulatory
49+
elements, line strings, polygons, and points are all copied, and points shared
50+
between maps (same ID) are deduplicated so that successor/predecessor
51+
relationships are preserved.
52+
4. Write the merged map with `lanelet::write` using the same projector.
53+
54+
## Relation to `autoware_lanelet2_map_divider`
55+
56+
Running the merger on the output of the divider reconstructs the original map.
57+
For example:
58+
59+
```bash
60+
# divide
61+
ros2 launch autoware_lanelet2_map_divider lanelet2_map_divider.launch.xml \
62+
input_lanelet2_map:=/map/lanelet2_map.osm \
63+
output_lanelet2_map_dir:=/map_divided \
64+
map_projector_info_path:=/map/map_projector_info.yaml
65+
66+
# merge back
67+
ros2 launch autoware_lanelet2_map_merger lanelet2_map_merger.launch.xml \
68+
input_lanelet2_map_dir:=/map_divided/lanelet2_map.osm \
69+
output_lanelet2_map:=/map/lanelet2_map_merged.osm \
70+
map_projector_info_path:=/map/map_projector_info.yaml
71+
```
72+
73+
## LICENSE
74+
75+
Apache License 2.0.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**:
2+
ros__parameters:
3+
input_lanelet2_map_dir: "$(var input_lanelet2_map_dir)"
4+
output_lanelet2_map: "$(var output_lanelet2_map)"
5+
map_projector_info_path: "$(var map_projector_info_path)"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<launch>
2+
<arg name="config_file_path" default="$(find-pkg-share autoware_lanelet2_map_merger)/config/lanelet2_map_merger.param.yaml" description="Path to the configuration YAML file"/>
3+
<arg name="input_lanelet2_map_dir" description="Directory containing the input .osm files"/>
4+
<arg name="output_lanelet2_map" description="Path of the output merged .osm file"/>
5+
<arg name="map_projector_info_path" description="Path to the map_projector_info.yaml file"/>
6+
7+
<group>
8+
<node pkg="autoware_lanelet2_map_merger" exec="autoware_lanelet2_map_merger_node" name="lanelet2_map_merger" output="screen">
9+
<param from="$(var config_file_path)" allow_substs="true"/>
10+
<param name="input_lanelet2_map_dir" value="$(var input_lanelet2_map_dir)"/>
11+
<param name="output_lanelet2_map" value="$(var output_lanelet2_map)"/>
12+
<param name="map_projector_info_path" value="$(var map_projector_info_path)"/>
13+
</node>
14+
</group>
15+
</launch>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>autoware_lanelet2_map_merger</name>
5+
<version>0.6.0</version>
6+
<description>A package for merging divided Lanelet2 (.osm) map files into a single .osm file</description>
7+
<maintainer email="ryohsuke.mitsudome@tier4.jp">Ryohsuke Mitsudome</maintainer>
8+
<license>Apache License 2.0</license>
9+
10+
<author email="ryohsuke.mitsudome@tier4.jp">Ryohsuke Mitsudome</author>
11+
12+
<buildtool_depend>ament_cmake_auto</buildtool_depend>
13+
<buildtool_depend>autoware_cmake</buildtool_depend>
14+
15+
<depend>autoware_geography_utils</depend>
16+
<depend>autoware_lanelet2_extension</depend>
17+
<depend>autoware_map_msgs</depend>
18+
<depend>autoware_map_projection_loader</depend>
19+
<depend>rclcpp</depend>
20+
<depend>rclcpp_components</depend>
21+
22+
<exec_depend>ros2launch</exec_depend>
23+
24+
<test_depend>ament_cmake_gtest</test_depend>
25+
<test_depend>ament_lint_auto</test_depend>
26+
<test_depend>autoware_lint_common</test_depend>
27+
28+
<export>
29+
<build_type>ament_cmake</build_type>
30+
</export>
31+
</package>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Parameters for autoware lanelet2 map merger node",
4+
"type": "object",
5+
"definitions": {
6+
"autoware_lanelet2_map_merger": {
7+
"type": "object",
8+
"properties": {
9+
"input_lanelet2_map_dir": {
10+
"type": "string",
11+
"description": "Path to the directory containing the input .osm files",
12+
"default": ""
13+
},
14+
"output_lanelet2_map": {
15+
"type": "string",
16+
"description": "Path to the merged output .osm file",
17+
"default": ""
18+
},
19+
"map_projector_info_path": {
20+
"type": "string",
21+
"description": "Path to the map_projector_info.yaml file",
22+
"default": ""
23+
}
24+
},
25+
"required": ["input_lanelet2_map_dir", "output_lanelet2_map", "map_projector_info_path"],
26+
"additionalProperties": false
27+
}
28+
},
29+
"properties": {
30+
"/**": {
31+
"type": "object",
32+
"properties": {
33+
"ros__parameters": {
34+
"$ref": "#/definitions/autoware_lanelet2_map_merger"
35+
}
36+
},
37+
"required": ["ros__parameters"],
38+
"additionalProperties": false
39+
}
40+
},
41+
"required": ["/**"],
42+
"additionalProperties": false
43+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright 2026 Autoware Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "lanelet2_map_merger.hpp"
16+
17+
#include "local_projector.hpp"
18+
19+
#include <autoware/geography_utils/lanelet2_projector.hpp>
20+
#include <autoware/map_projection_loader/map_projection_loader.hpp>
21+
#include <autoware_lanelet2_extension/io/autoware_osm_parser.hpp>
22+
23+
#include <lanelet2_io/Io.h>
24+
25+
#include <algorithm>
26+
#include <filesystem>
27+
#include <memory>
28+
#include <string>
29+
#include <vector>
30+
31+
namespace fs = std::filesystem;
32+
33+
namespace autoware::lanelet2_map_merger
34+
{
35+
36+
namespace
37+
{
38+
39+
bool is_osm_file(const fs::path & path)
40+
{
41+
if (fs::is_directory(path)) {
42+
return false;
43+
}
44+
const std::string ext = path.extension().string();
45+
return ext == ".osm" || ext == ".OSM";
46+
}
47+
48+
} // namespace
49+
50+
std::vector<std::string> Lanelet2MapMerger::discover_osm_files(const std::string & input_dir) const
51+
{
52+
std::vector<std::string> osm_files;
53+
fs::path input_path(input_dir);
54+
55+
if (!fs::exists(input_path) || !fs::is_directory(input_path)) {
56+
RCLCPP_ERROR(logger_, "Input is not a valid directory: %s", input_dir.c_str());
57+
return osm_files;
58+
}
59+
60+
for (const auto & entry : fs::directory_iterator(input_path)) {
61+
if (is_osm_file(entry.path())) {
62+
osm_files.push_back(entry.path().string());
63+
}
64+
}
65+
std::sort(osm_files.begin(), osm_files.end());
66+
67+
RCLCPP_INFO(logger_, "Found %zu .osm files in %s", osm_files.size(), input_dir.c_str());
68+
return osm_files;
69+
}
70+
71+
std::unique_ptr<lanelet::Projector> Lanelet2MapMerger::create_projector(
72+
const autoware_map_msgs::msg::MapProjectorInfo & projector_info) const
73+
{
74+
if (projector_info.projector_type == autoware_map_msgs::msg::MapProjectorInfo::LOCAL) {
75+
return std::make_unique<LocalProjector>();
76+
}
77+
return autoware::geography_utils::get_lanelet2_projector(projector_info);
78+
}
79+
80+
lanelet::LaneletMapPtr Lanelet2MapMerger::load_and_merge_maps(
81+
const std::vector<std::string> & osm_files,
82+
const autoware_map_msgs::msg::MapProjectorInfo & projector_info,
83+
lanelet::Projector & projector)
84+
{
85+
auto merged = std::make_shared<lanelet::LaneletMap>();
86+
87+
const bool is_local =
88+
projector_info.projector_type == autoware_map_msgs::msg::MapProjectorInfo::LOCAL;
89+
90+
for (const auto & path : osm_files) {
91+
RCLCPP_INFO(logger_, "Loading %s", path.c_str());
92+
lanelet::ErrorMessages errors;
93+
lanelet::LaneletMapPtr map =
94+
lanelet::load(path, "autoware_osm_handler", projector, &errors);
95+
for (const auto & error : errors) {
96+
RCLCPP_ERROR(logger_, "Error loading %s: %s", path.c_str(), error.c_str());
97+
}
98+
if (!errors.empty() || !map) {
99+
return nullptr;
100+
}
101+
102+
if (is_local) {
103+
for (lanelet::Point3d point : map->pointLayer) {
104+
if (point.hasAttribute("local_x")) {
105+
point.x() = point.attribute("local_x").asDouble().value();
106+
}
107+
if (point.hasAttribute("local_y")) {
108+
point.y() = point.attribute("local_y").asDouble().value();
109+
}
110+
}
111+
}
112+
113+
// Merge with deduplication of shared points (same id), matching
114+
// lanelet2_map_loader's merge_lanelet2_maps behavior.
115+
for (lanelet::Lanelet & llt : map->laneletLayer) {
116+
merged->add(llt);
117+
}
118+
for (lanelet::Area & area : map->areaLayer) {
119+
merged->add(area);
120+
}
121+
for (lanelet::RegulatoryElementPtr & reg : map->regulatoryElementLayer) {
122+
merged->add(reg);
123+
}
124+
for (lanelet::LineString3d & ls : map->lineStringLayer) {
125+
for (lanelet::Point3d & pt : ls) {
126+
if (merged->pointLayer.find(pt.id()) != merged->pointLayer.end()) {
127+
pt = merged->pointLayer.get(pt.id());
128+
}
129+
}
130+
merged->add(ls);
131+
}
132+
for (lanelet::Polygon3d & poly : map->polygonLayer) {
133+
merged->add(poly);
134+
}
135+
for (lanelet::Point3d & pt : map->pointLayer) {
136+
if (merged->pointLayer.find(pt.id()) == merged->pointLayer.end()) {
137+
merged->add(pt);
138+
}
139+
}
140+
141+
// Keep the source map alive so weak references from regulatory elements
142+
// (e.g., parameters referencing other lanelets/linestrings) do not expire.
143+
loaded_maps_.push_back(map);
144+
}
145+
146+
return merged;
147+
}
148+
149+
void Lanelet2MapMerger::run()
150+
{
151+
const auto osm_files = discover_osm_files(input_dir_);
152+
if (osm_files.empty()) {
153+
RCLCPP_ERROR(logger_, "No .osm files found under %s", input_dir_.c_str());
154+
return;
155+
}
156+
157+
autoware_map_msgs::msg::MapProjectorInfo projector_info;
158+
try {
159+
projector_info =
160+
autoware::map_projection_loader::load_info_from_yaml(map_projector_info_path_);
161+
} catch (const std::exception & e) {
162+
RCLCPP_ERROR(
163+
logger_, "Failed to load map projector info from %s: %s", map_projector_info_path_.c_str(),
164+
e.what());
165+
return;
166+
}
167+
RCLCPP_INFO(logger_, "Projector type: %s", projector_info.projector_type.c_str());
168+
169+
std::unique_ptr<lanelet::Projector> projector;
170+
try {
171+
projector = create_projector(projector_info);
172+
} catch (const std::exception & e) {
173+
RCLCPP_ERROR(logger_, "Failed to create projector: %s", e.what());
174+
return;
175+
}
176+
177+
lanelet::LaneletMapPtr merged = load_and_merge_maps(osm_files, projector_info, *projector);
178+
if (!merged) {
179+
RCLCPP_ERROR(logger_, "Failed to load Lanelet2 maps from %s", input_dir_.c_str());
180+
return;
181+
}
182+
183+
const fs::path out_path(output_);
184+
if (out_path.has_parent_path()) {
185+
fs::create_directories(out_path.parent_path());
186+
}
187+
if (fs::exists(out_path)) {
188+
fs::remove(out_path);
189+
}
190+
191+
lanelet::write(output_, *merged, *projector);
192+
RCLCPP_INFO(logger_, "Saved merged map: %s", output_.c_str());
193+
}
194+
195+
} // namespace autoware::lanelet2_map_merger

0 commit comments

Comments
 (0)