CARLA
Loading...
Searching...
No Matches
test_opendrive.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
7#include "test.h"
8#include "OpenDrive.h"
9#include "Random.h"
10
11#include <carla/StopWatch.h>
12#include <carla/geom/Location.h>
13#include <carla/geom/Math.h>
20
22
23#include <fstream>
24#include <string>
25
26using namespace carla::road;
27using namespace carla::road::element;
28using namespace carla::geom;
29using namespace carla::opendrive;
30using namespace util;
31
32const std::string BASE_PATH = LIBCARLA_TEST_CONTENT_FOLDER "/OpenDrive/";
33
34// Road Elevation
35static void test_road_elevation(const pugi::xml_document &xml, std::optional<Map>& map) {
36 pugi::xml_node open_drive_node = xml.child("OpenDRIVE");
37
38 for (pugi::xml_node road_node : open_drive_node.children("road")) {
39 RoadId road_id = road_node.attribute("id").as_uint();
40 auto elevation_profile_nodes = road_node.children("elevationProfile");
41
42 for (pugi::xml_node elevation_profile_node : elevation_profile_nodes) {
43 auto total_elevations = 0;
44 auto elevation_nodes = elevation_profile_node.children("elevation");
45 auto total_elevation_parser = std::distance(elevation_nodes.begin(), elevation_nodes.end());
46
47 for (pugi::xml_node elevation_node : elevation_nodes) {
48 float s = elevation_node.attribute("s").as_float();
49 const auto elevation = map->GetMap().GetRoad(road_id).GetInfo<RoadInfoElevation>(s);
50 if (elevation != nullptr)
51 ++total_elevations;
52 }
53 ASSERT_EQ(total_elevations, total_elevation_parser);
54 }
55 }
56}
57
58// Geometry
59static void test_geometry(const pugi::xml_document &xml, std::optional<Map>& map) {
60 pugi::xml_node open_drive_node = xml.child("OpenDRIVE");
61
62 for (pugi::xml_node road_node : open_drive_node.children("road")) {
63 RoadId road_id = road_node.attribute("id").as_uint();
64
65 for (pugi::xml_node plan_view_nodes : road_node.children("planView")) {
66 auto geometries_parser = plan_view_nodes.children("geometry");
67 auto total_geometries_parser = std::distance(geometries_parser.begin(), geometries_parser.end());
68 auto total_geometries = 0;
69 for (pugi::xml_node geometry_node : plan_view_nodes.children("geometry")){
70 float s = geometry_node.attribute("s").as_float();
71 auto geometry = map->GetMap().GetRoad(road_id).GetInfo<RoadInfoGeometry>(s);
72 if (geometry != nullptr)
73 ++total_geometries;
74 }
75 ASSERT_EQ(total_geometries, total_geometries_parser);
76 }
77 }
78}
79
80// Road test
83 LaneSection& lane_section) {
84 constexpr auto error = 1e-5;
85 auto total_road_mark = 0;
86 auto total_road_mark_parser = 0;
87 for (pugi::xml_node lane_node : lane_nodes) {
88 // Check Road Mark
89 auto road_mark_nodes = lane_node.children("roadMark");
90 total_road_mark_parser += std::distance(road_mark_nodes.begin(), road_mark_nodes.end());
91
92 const int lane_id = lane_node.attribute("id").as_int();
93 Lane* lane = nullptr;
94 lane = lane_section.GetLane(lane_id);
95 EXPECT_NE(lane, nullptr);
96
97 // <roadMark sOffset="0.0000000000000000e+0" type="none" material="standard" color="white" laneChange="none"/>
98 for (pugi::xml_node road_mark_node : road_mark_nodes) {
99 const auto s_offset = road_mark_node.attribute("sOffset").as_double();
100 const auto type = road_mark_node.attribute("type").as_string();
101 const auto material = road_mark_node.attribute("material").as_string();
102 const auto color = road_mark_node.attribute("color").as_string();
103 const auto road_mark = lane->GetInfo<RoadInfoMarkRecord>(lane->GetDistance() + s_offset);
104 if (road_mark != nullptr) {
105 EXPECT_NEAR(lane->GetDistance() + s_offset, road_mark->GetDistance(), error);
106 EXPECT_EQ(type, road_mark->GetType());
107 EXPECT_EQ(material, road_mark->GetMaterial());
108 EXPECT_EQ(color, road_mark->GetColor());
109 ++total_road_mark;
110 }
111 }
112 }
113 return std::make_pair(total_road_mark, total_road_mark_parser);
114}
115
116static void test_roads(const pugi::xml_document &xml, std::optional<Map>& map) {
117 pugi::xml_node open_drive_node = xml.child("OpenDRIVE");
118
119 // Check total Roads
120 auto roads_parser = open_drive_node.children("road");
121 auto total_roads_parser = std::distance(roads_parser.begin(), roads_parser.end());
122 auto total_roads = map->GetMap().GetRoads().size();
123 ASSERT_EQ(total_roads, total_roads_parser);
124
125 for (pugi::xml_node road_node : roads_parser) {
126 RoadId road_id = road_node.attribute("id").as_uint();
127
128 for (pugi::xml_node lanes_node : road_node.children("lanes")) {
129
130 // Check total Lane Sections
131 auto lane_sections_parser = lanes_node.children("laneSection");
132 auto total_lane_sections_parser = std::distance(lane_sections_parser.begin(), lane_sections_parser.end());
133 auto total_lane_sections = map->GetMap().GetRoad(road_id).GetLaneSections().size();
134 ASSERT_EQ(total_lane_sections, total_lane_sections_parser);
135
136 for (pugi::xml_node lane_section_node : lane_sections_parser) {
137
138 // Check total Lanes
139 const double s = lane_section_node.attribute("s").as_double();
140 auto lane_section = map->GetMap().GetRoad(road_id).GetLaneSectionsAt(s);
141 size_t total_lanes = 0u;
142 for (auto it = lane_section.begin(); it != lane_section.end(); ++it) {
143 total_lanes = it->GetLanes().size();
144 }
145 auto left_nodes = lane_section_node.child("left").children("lane");
146 auto center_nodes = lane_section_node.child("center").children("lane");
147 auto right_nodes = lane_section_node.child("right").children("lane");
148 auto total_lanes_parser = std::distance(left_nodes.begin(), left_nodes.end());
149 total_lanes_parser += std::distance(right_nodes.begin(), right_nodes.end());
150 total_lanes_parser += std::distance(center_nodes.begin(), center_nodes.end());
151
152 ASSERT_EQ(total_lanes, total_lanes_parser);
153
154
155 auto total_road_mark = 0;
156 auto total_road_mark_parser = 0;
157 for (auto it = lane_section.begin(); it != lane_section.end(); ++it) {
158 auto total_left = get_total_road_marks(left_nodes, *it);
159 auto total_center = get_total_road_marks(center_nodes, *it);
160 auto total_right = get_total_road_marks(right_nodes, *it);
161 total_road_mark = total_left.first + total_center.first + total_right.first;
162 total_road_mark_parser = total_left.first + total_center.first + total_right.first;
163 }
164 ASSERT_EQ(total_road_mark, total_road_mark_parser);
165 }
166 }
167 }
168}
169
170// Junctions
171static void test_junctions(const pugi::xml_document &xml, std::optional<Map>& map) {
172 pugi::xml_node open_drive_node = xml.child("OpenDRIVE");
173
174 // Check total number of junctions
175 auto& junctions = map->GetMap().GetJunctions();
176 auto total_junctions_parser = std::distance(open_drive_node.children("junction").begin(), open_drive_node.children("junction").end());
177
178 ASSERT_EQ(junctions.size(), total_junctions_parser);
179
180 for (pugi::xml_node junction_node : open_drive_node.children("junction")) {
181 // Check total number of connections
182 auto total_connections_parser = std::distance(junction_node.children("connection").begin(), junction_node.children("connection").end());
183
184 JuncId junction_id = junction_node.attribute("id").as_int();
185 auto& junction = junctions.find(junction_id)->second;
186
187 auto& connections = junction.GetConnections();
188
189 ASSERT_EQ(connections.size(), total_connections_parser);
190
191 for (pugi::xml_node connection_node : junction_node.children("connection")) {
192 auto total_lane_links_parser = std::distance(connection_node.children("laneLink").begin(), connection_node.children("laneLink").end());
193
194 ConId connection_id = connection_node.attribute("id").as_uint();
195 auto& connection = connections.find(connection_id)->second;
196
197 auto& lane_links = connection.lane_links;
198
199 ASSERT_EQ(lane_links.size(), total_lane_links_parser);
200
201 }
202 }
203}
204
205static void test_road_links(std::optional<Map>& map) {
206
207 // process all roads, sections and lanes
208 for (auto &road : map->GetMap().GetRoads()) {
209 for (auto &section : road.second.GetLaneSections()) {
210 for (auto &lane : section.GetLanes()) {
211 // check all nexts
212 for (auto link : lane.second.GetNextLanes()) {
213 ASSERT_TRUE(link != nullptr);
214 }
215 // check all prevs
216 for (auto link : lane.second.GetPreviousLanes()) {
217 ASSERT_TRUE(link != nullptr);
218 }
219 }
220 }
221 }
222}
223
224// A valid OpenDRIVE file may legitimately contain no road at all: CARLA ships
225// EmptyMap.xodr, a header-only file, so that world.get_map() works on the empty
226// sandbox map. Geometry tests have nothing to assert on such a map.
227static bool IsRoadless(carla::road::Map &map) {
228 return map.GetMap().GetRoads().empty();
229}
230
231TEST(road, parse_files) {
232 for (const auto &file : util::OpenDrive::GetAvailableFiles()) {
233 // std::cerr << file << std::endl;
235 ASSERT_TRUE(map);
236 // print_roads(map, file);
237 }
238}
239
240TEST(road, parse_road_links) {
241 for (const auto &file : util::OpenDrive::GetAvailableFiles()) {
242 // std::cerr << file << std::endl;
244 ASSERT_TRUE(map);
245 test_road_links(map);
246 }
247}
248
249TEST(road, parse_junctions) {
250 for (const auto& file : util::OpenDrive::GetAvailableFiles()) {
252 ASSERT_TRUE(map.has_value());
253
254 const std::string full_path = BASE_PATH + file;
256
257 pugi::xml_parse_result result = xml.load_file( full_path.c_str());
258 ASSERT_TRUE(result);
259
260 test_junctions(xml, map);
261 }
262
263}
264
265TEST(road, parse_road) {
266 for (const auto& file : util::OpenDrive::GetAvailableFiles()) {
268 ASSERT_TRUE(map.has_value());
269
270 const std::string full_path = BASE_PATH + file;
272 pugi::xml_parse_result result = xml.load_file( full_path.c_str());
273 ASSERT_TRUE(result);
274
275 test_roads(xml, map);
276 }
277
278}
279
280TEST(road, parse_road_elevation) {
281 for (const auto& file : util::OpenDrive::GetAvailableFiles()) {
283 ASSERT_TRUE(map.has_value());
284
285 const std::string full_path = BASE_PATH + file;
287 pugi::xml_parse_result result = xml.load_file( full_path.c_str());
288 ASSERT_TRUE(result);
289
290 test_road_elevation(xml, map);
291 }
292
293}
294
295TEST(road, parse_geometry) {
296 for (const auto& file : util::OpenDrive::GetAvailableFiles()) {
298 ASSERT_TRUE(map.has_value());
299
300 const std::string full_path = BASE_PATH + file;
302 pugi::xml_parse_result result = xml.load_file( full_path.c_str());
303 ASSERT_TRUE(result);
304
305 test_geometry(xml, map);
306 }
307
308}
309
310TEST(road, iterate_waypoints) {
311 for (const auto& file : util::OpenDrive::GetAvailableFiles()) {
312 carla::logging::log("Parsing", file);
313 carla::StopWatch stop_watch;
315 ASSERT_TRUE(m.has_value());
316 auto &map = *m;
317 if (IsRoadless(map)) {
318 carla::logging::log(" no roads, skipping:", file);
319 continue;
320 }
321 const auto topology = map.GenerateTopology();
322 ASSERT_FALSE(topology.empty());
323 auto count = 0u;
324 auto waypoints = map.GenerateWaypoints(0.5);
325 ASSERT_FALSE(waypoints.empty());
326 Random::Shuffle(waypoints);
327 const auto number_of_waypoints_to_explore =
328 std::min<size_t>(2000u, waypoints.size());
329 for (auto i = 0u; i < number_of_waypoints_to_explore; ++i) {
330 auto wp = waypoints[i];
331 map.ComputeTransform(wp);
332 if (i != 0u) {
333 ASSERT_NE(wp, waypoints[0u]);
334 }
335 for (auto &&successor : map.GetSuccessors(wp)) {
336 ASSERT_TRUE(
337 successor.road_id != wp.road_id ||
338 successor.section_id != wp.section_id ||
339 successor.lane_id != wp.lane_id ||
340 successor.s != wp.s);
341 }
342 auto origin = wp;
343 for (auto j = 0u; j < 200u; ++j) {
344 auto next_wps = map.GetNext(origin, Random::Uniform(0.0001, 150.0));
345 if (next_wps.empty()) {
346 break;
347 }
348 const auto number_of_next_wps_to_explore =
349 std::min<size_t>(10u, next_wps.size());
350 Random::Shuffle(next_wps);
351 for (auto k = 0u; k < number_of_next_wps_to_explore; ++k) {
352 auto next = next_wps[k];
353 ++count;
354 ASSERT_TRUE(
355 next.road_id != wp.road_id ||
356 next.section_id != wp.section_id ||
357 next.lane_id != wp.lane_id ||
358 next.s != wp.s);
359 auto right = map.GetRight(next);
360 if (right.has_value()) {
361 ASSERT_EQ(right->road_id, next.road_id);
362 ASSERT_EQ(right->section_id, next.section_id);
363 ASSERT_NE(right->lane_id, next.lane_id);
364 ASSERT_EQ(right->s, next.s);
365 }
366 auto left = map.GetLeft(next);
367 if (left.has_value()) {
368 ASSERT_EQ(left->road_id, next.road_id);
369 ASSERT_EQ(left->section_id, next.section_id);
370 ASSERT_NE(left->lane_id, next.lane_id);
371 ASSERT_EQ(left->s, next.s);
372 }
373 }
374 origin = next_wps[0u];
375 }
376 }
377 ASSERT_GT(count, 0u);
378 float seconds = 1e-3f * static_cast<float>(stop_watch.GetElapsedTime());
379 carla::logging::log(file, "done in", seconds, "seconds.");
380 }
381}
382
383TEST(road, get_waypoint) {
384 for (const auto& file : util::OpenDrive::GetAvailableFiles()) {
385 carla::logging::log("Parsing", file);
386 carla::StopWatch stop_watch;
388 ASSERT_TRUE(m.has_value());
389 auto &map = *m;
390 if (IsRoadless(map)) {
391 carla::logging::log(" no roads, skipping:", file);
392 continue;
393 }
394 for (auto i = 0u; i < 10'000u; ++i) {
395 const auto location = Random::Location(-500.0f, 500.0f);
396 auto owp = map.GetClosestWaypointOnRoad(location);
397 ASSERT_TRUE(owp.has_value());
398 auto &wp = *owp;
399 for (auto &next : map.GetNext(wp, 0.5)) {
400 ASSERT_TRUE(
401 next.road_id != wp.road_id ||
402 next.section_id != wp.section_id ||
403 next.lane_id != wp.lane_id ||
404 next.s != wp.s);
405 }
406 auto left = map.GetLeft(wp);
407 if (left.has_value()) {
408 ASSERT_EQ(left->road_id, wp.road_id);
409 ASSERT_EQ(left->section_id, wp.section_id);
410 ASSERT_NE(left->lane_id, wp.lane_id);
411 ASSERT_EQ(left->s, wp.s);
412 }
413 auto right = map.GetRight(wp);
414 if (right.has_value()) {
415 ASSERT_EQ(right->road_id, wp.road_id);
416 ASSERT_EQ(right->section_id, wp.section_id);
417 ASSERT_NE(right->lane_id, wp.lane_id);
418 ASSERT_EQ(right->s, wp.s);
419 }
420 }
421 float seconds = 1e-3f * static_cast<float>(stop_watch.GetElapsedTime());
422 carla::logging::log(file, "done in", seconds, "seconds.");
423 }
424}
425
426// Minimal OpenDRIVE with a single road (id=1) carrying one stencil <object>.
427// The stencil object is named "Stencil_Arrow" so the parser keys on the
428// "Stencil_" prefix while the existing "Stencil_STOP" stop-line branch is
429// left untouched.
430static constexpr const char* kStencilXodr = R"(<?xml version="1.0" encoding="UTF-8"?>
431<OpenDRIVE>
432 <header revMajor="1" revMinor="4" name="" version="1" date="2026-01-01T00:00:00" north="0.0" south="0.0" east="0.0" west="0.0" vendor="CARLA"/>
433 <road name="Road 1" length="2.1500000000000000e+1" id="1" junction="-1">
434 <type s="0.0000000000000000e+0" type="town">
435 <speed max="35" unit="mph"/>
436 </type>
437 <planView>
438 <geometry s="0.0000000000000000e+0" x="0.0000000000000000e+0" y="0.0000000000000000e+0" hdg="0.0000000000000000e+0" length="2.1500000000000000e+1">
439 <line/>
440 </geometry>
441 </planView>
442 <elevationProfile>
443 <elevation s="0.0000000000000000e+0" a="0.0000000000000000e+0" b="0.0000000000000000e+0" c="0.0000000000000000e+0" d="0.0000000000000000e+0"/>
444 </elevationProfile>
445 <lanes>
446 <laneOffset s="0.0000000000000000e+0" a="0.0000000000000000e+0" b="0.0000000000000000e+0" c="0.0000000000000000e+0" d="0.0000000000000000e+0"/>
447 <laneSection s="0.0000000000000000e+0" singleSide="false">
448 <center>
449 <lane id="0" type="none" level="false">
450 <roadMark sOffset="0.0000000000000000e+0" type="none" material="standard" color="white" laneChange="none"/>
451 </lane>
452 </center>
453 <right>
454 <lane id="-1" type="driving" level="false">
455 <width sOffset="0.0000000000000000e+0" a="3.5000000000000000e+0" b="0.0000000000000000e+0" c="0.0000000000000000e+0" d="0.0000000000000000e+0"/>
456 <roadMark sOffset="0.0000000000000000e+0" type="none" material="standard" color="white" laneChange="none"/>
457 <speed sOffset="0.0000000000000000e+0" max="3.5000000000000000e+1" unit="mph"/>
458 </lane>
459 </right>
460 </laneSection>
461 </lanes>
462 <objects>
463 <object id="100" name="Stencil_Arrow" s="5.0000000000000000e+0" t="0.0000000000000000e+0" zOffset="0.0000000000000000e+0" hdg="0.0000000000000000e+0" roll="0.0000000000000000e+0" pitch="0.0000000000000000e+0" orientation="none" type="stencil" length="2.0000000000000000e+0" width="1.0000000000000000e+0"/>
464 </objects>
465 </road>
466</OpenDRIVE>
467)";
468
469TEST(road, stencil_parsing) {
470 std::string xodr = kStencilXodr;
471 auto map = OpenDriveParser::Load(xodr);
472 ASSERT_TRUE(map.has_value());
473 auto stencils = map->GetAllStencilReferences();
474 ASSERT_EQ(stencils.size(), 1u);
475 EXPECT_EQ(stencils[0]->GetRoadId(), 1u);
476
477 // The build finalization must populate the map-level stencil container and
478 // link each RoadInfoStencil to the corresponding Stencil.
479 const auto &map_stencils = map->GetStencils();
480 ASSERT_EQ(map_stencils.size(), 1u);
481 const auto stencil_id = stencils[0]->GetStencilId();
482 auto it = map_stencils.find(stencil_id);
483 ASSERT_NE(it, map_stencils.end());
484 EXPECT_EQ(stencils[0]->GetStencil(), it->second.get());
485
486 // A search that runs past the lane end must still return the stencils
487 // before the lane end.
488 const Waypoint start{1u, 0u, -1, 0.0};
489 auto found = map->GetStencilsInDistance(start, 30.0, false);
490 ASSERT_EQ(found.size(), 1u);
491 EXPECT_DOUBLE_EQ(found[0].accumulated_s, 5.0);
492}
float elevation
std::string road_id
Definition LaneGraph.cpp:39
std::map< std::string, unsigned int > road
std::map< std::string, unsigned int > junction
size_t GetElapsedTime() const
Definition StopWatch.h:39
static std::optional< road::Map > Load(const std::string &opendrive)
Lane * GetLane(const LaneId id)
const T * GetInfo(const double s) const
Definition road/Lane.h:79
double GetDistance() const
Definition road/Lane.cpp:46
Each lane within a road cross section can be provided with several road markentries.
unsigned int as_uint(unsigned int def=0) const
Definition pugixml.cpp:5173
xml_parse_result load_file(const char *path, unsigned int options=parse_default, xml_encoding encoding=encoding_auto)
Definition pugixml.cpp:7108
xml_node child(const char_t *name) const
Definition pugixml.cpp:5490
xml_object_range< xml_node_iterator > children() const
Definition pugixml.cpp:5425
xml_attribute attribute(const char_t *name) const
Definition pugixml.cpp:5500
static std::vector< std::string > GetAvailableFiles()
static std::string Load(const std::string &filename)
static double Uniform(double min, double max)
Definition Random.h:19
static void Shuffle(RangeT &range)
Definition Random.h:30
static carla::geom::Location Location(float min, float max)
Definition Random.h:24
static void log(Args &&... args)
Definition Logging.h:59
int32_t JuncId
Definition RoadTypes.h:17
uint32_t RoadId
Definition RoadTypes.h:15
uint32_t ConId
Definition RoadTypes.h:29
const std::string BASE_PATH
static void test_road_links(std::optional< Map > &map)
TEST(road, parse_files)
static void test_roads(const pugi::xml_document &xml, std::optional< Map > &map)
static void test_junctions(const pugi::xml_document &xml, std::optional< Map > &map)
static bool IsRoadless(carla::road::Map &map)
static void test_geometry(const pugi::xml_document &xml, std::optional< Map > &map)
static auto get_total_road_marks(pugi::xml_object_range< pugi::xml_named_node_iterator > &lane_nodes, LaneSection &lane_section)
static void test_road_elevation(const pugi::xml_document &xml, std::optional< Map > &map)
static constexpr const char * kStencilXodr