Lanelet2 Maps
The CPM Lab describes its tracks with Lanelet2 maps. This page explains the format as far as you need it for a motion planner, how a planner receives the map, and how to create your own map.
Lanelet2 in a Nutshell
A Lanelet2 map is stored as an OpenStreetMap (.osm) XML file and consists of three layers:
Points: positions.
Linestrings: ordered lists of points, for example a lane marking, a curb or a stop line. The
typeandsubtypeattributes say what they are, e.g.line_thin/dashed.Lanelets: the drivable lane sections. Each lanelet has a left and a right boundary linestring and a driving direction along them. Lanelets that share a boundary are neighbors; a lanelet whose start is the end of another lanelet is its successor.
On top of these, regulatory elements describe rules that apply to lanelets, for example speed limits, and areas (multipolygons) describe non-drivable regions such as vegetation or keep-out zones.
From the lanelets and the traffic rules, Lanelet2 builds a routing graph. It answers questions such as “which lanelets follow this one?” or “what is the shortest route from A to B?”. The CPM Lab planners use the German traffic rules for vehicles.
Coordinates
The map files store latitude and longitude. The map server converts them with the CpmProjector of
cpm_lab_lanelet2_extension_cpp into the lab coordinate system:
Every map has a node tagged
origin=true, which becomes(0, 0), and a node taggedorientation=true, which defines the direction of the positive x axis.The distances are divided by 18, the scale of the lab. A map drawn at real-world size in JOSM therefore becomes a 1:18 model, and all map coordinates are in lab meters.
Speed limits in the map are real-world values in km/h. Divide them by
18 * 3.6to get lab speeds in m/s, as the built-in planners do.
See Interface Reference for the frames and units of the whole system.
Available Maps
The maps are installed with the cpm_lab_maps package:
Map |
Lanelets |
Content |
|---|---|---|
|
184 |
The complete lab track with urban roads, intersections and keep-out areas. Default map. |
|
128 |
The lab track with fewer intersections. |
|
40 |
Only the outer loop of the lab track, with a speed limit. |
|
116 |
The lab track with speed limits, traffic signs and turn arrows, for planners that follow traffic rules. |
|
25 |
A section of the A61 highway with spawn points for vehicles 1 to 10. |
|
11 |
A reduced A61 section with speed limits and spawn points, used by the SFB planner. |
Receiving the Map in a Motion Planner
The map server publishes the map once as cpm_lab_map_msgs/msg/Lanelet2MapBin on /lab/map. The bridge forwards
it to /planner/map. Both topics are transient local, so a late subscriber still receives the last map if it
subscribes with the same durability:
Field |
Description |
|---|---|
|
|
|
Always |
|
The serialized Lanelet2 map. |
If your planner subscribes without transient local durability, it has to request the map: send a MAP_REQUEST
system trigger on /planner/system_trigger with a source_id starting with planner. The bridge forwards the
request and the map server publishes the map again.
Python
cpm_lab_lanelet2_extension_py provides from_bin_msg, which expects the message data as Python list:
import lanelet2
import cpm_lab_lanelet2_extension_py
from lanelet2.routing import RoutingGraph
from lanelet2.traffic_rules import Locations, Participants
from rclpy.qos import QoSProfile, DurabilityPolicy
from cpm_lab_map_msgs.msg import Lanelet2MapBin
# In the constructor of your node
qos = QoSProfile(depth=1, durability=DurabilityPolicy.TRANSIENT_LOCAL)
self.create_subscription(Lanelet2MapBin, 'map', self.on_map, qos)
def on_map(self, message):
self.lanelet_map = cpm_lab_lanelet2_extension_py.from_bin_msg(list(message.data))
traffic_rules = lanelet2.traffic_rules.create(Locations.Germany, Participants.Vehicle)
self.routing_graph = RoutingGraph(self.lanelet_map, traffic_rules)
Import lanelet2 before calling from_bin_msg, otherwise Python cannot handle the returned map.
C++
cpm_lab_lanelet2_extension_cpp provides lanelet::utils::conversion::fromBinMsg:
#include <cpm_lab_lanelet2_extension_cpp/message_conversion.hpp>
#include <lanelet2_routing/RoutingGraph.h>
#include <lanelet2_traffic_rules/TrafficRulesFactory.h>
auto map = std::make_shared<lanelet::LaneletMap>();
lanelet::utils::conversion::fromBinMsg(message, map);
auto traffic_rules = lanelet::traffic_rules::TrafficRulesFactory::create(
lanelet::Locations::Germany, lanelet::Participants::Vehicle);
auto routing_graph = lanelet::routing::RoutingGraph::build(*map, *traffic_rules);
Link against cpm_lab_lanelet2_extension_cpp, lanelet2_core, lanelet2_io, lanelet2_traffic_rules and
lanelet2_routing, see cpm_lab_central_routing for a complete CMakeLists.txt.
Working with the Map
The most useful functions for a planner, shown in Python:
from lanelet2.core import BasicPoint2d
from lanelet2.geometry import findNearest, distance
# Lanelet closest to the vehicle position
position = BasicPoint2d(state.pose.position.x, state.pose.position.y)
nearest = findNearest(self.lanelet_map.laneletLayer, position, 1)[0][1]
# Lanelets that follow and the shortest route to a goal lanelet
following = self.routing_graph.following(nearest)
route = self.routing_graph.shortestPath(nearest, goal_lanelet)
# Center line of a lanelet as list of points, e.g. as path for PATH_TRACKING
centerline = [(p.x, p.y) for p in nearest.centerline]
# Speed limit of a lanelet in lab m/s
speed = traffic_rules.speedLimit(nearest).speedLimit / 3.6 / 18.0
The Lanelet2 Python tutorial shows the complete API.
Creating or Editing a Map
Maps are edited with JOSM and the Lanelet2 presets and styles, see the Lanelet2 map documentation.
Copy an existing map from
cpm_lab_maps/mapsas a starting point, so theoriginandorientationnodes are already in place.Draw the map at real-world scale. The projector divides all distances by 18.
Keep exactly one
originand oneorientationnode, see Reference Nodes.To define where simulated vehicles start, add a
virtuallinestring with two points per vehicle and the tagspawn_point=<vehicle_id>. The vehicle starts at the first point, heading towards the last point:<way id='-103114' visible='true'> <nd ref='-106580' /> <nd ref='-106581' /> <tag k='spawn_point' v='1' /> <tag k='type' v='virtual' /> </way>Make the IDs positive and validate the map, see Checking a Map.
Save the file as
.osmin~/cpm_lab/maps/(or$CPM_LAB_HOME/maps/) and restart the lab. The control flow lists these maps together with the installed ones, so the new map appears in the Control Center without rebuilding, also with the Debian packages and the Docker images, see Your Own Maps and IPS Configurations. Maps for all labs go intocpm_lab_maps/mapsof the source.
To test a map without rebuilding, start the map server manually with an absolute path:
export ROS_DOMAIN_ID=21
ros2 run cpm_lab_map_server cpm_lab_map_server_node --ros-args -p map_path:=/abs/path/my_map.osm -r __ns:=/lab
Reference Nodes
The CPM Lab uses two custom node tags that are not part of the Lanelet2 format. They define how the latitude and longitude of the map are converted into the lab coordinate system:
Tag |
Meaning |
|---|---|
|
This node becomes the origin |
|
The positive x axis points from the origin towards this node. Optional: without it the x axis points east. |
Only the key is evaluated; the value is true by convention. Place both nodes on the same edge of the track, usually
two corners of the lab rectangle, so the x axis runs along the track. In Lab Full.osm:
<node id="1939" visible="true" version="1" lat="48.07977196872" lon="11.63682417176">
<tag k="orientation" v="true" />
</node>
<node id="1940" visible="true" version="1" lat="48.07977196872" lon="11.63573501243">
<tag k="origin" v="true" />
</node>
In JOSM, select the node and add the tag origin or orientation with the value true in the tags panel. If
a map contains several nodes with the same tag, the map server uses the first one in the file.
Checking a Map
Make the IDs positive. JOSM gives new elements negative IDs until they are uploaded to OpenStreetMap, which never
happens for lab maps. Convert them to unique positive IDs with the script from lanelet2_python:
# Write the result to a new file
ros2 run lanelet2_python make_ids_positive.py my_map.osm my_map_positive.osm
# Or overwrite the input file
ros2 run lanelet2_python make_ids_positive.py my_map.osm --inplace
Open the result in JOSM afterwards and check that the origin and orientation tags are still set.
Validate the map. lanelet2_validate checks the map for common mapping errors, e.g. missing or wrong tags,
duplicated points, points that are too close, too sharp curves and lanelets that cannot be reached in the routing
graph. Pass the latitude and longitude of the origin node:
ros2 run lanelet2_validation lanelet2_validate my_map.osm --lat 48.07977196872 --lon 11.63573501243
The tool prints all errors and warnings. Fix the errors before you use the map; the map server refuses to load a map with loading errors. The validation runs on the real-world geometry, before the scaling by 18. Further options:
# Show all options, e.g. the location (default: de) and the traffic participants
ros2 run lanelet2_validation lanelet2_validate --help
# List the checks without running them
ros2 run lanelet2_validation lanelet2_validate my_map.osm --print
Note
Both tools are part of Lanelet2 and installed with ros-<ros2-distro>-lanelet2. On Ubuntu 26.04, where Lanelet2
is built from source (see Building from Source), lanelet2_validation is skipped by default; remove it from
--packages-skip to build it.