CARLA
Loading...
Searching...
No Matches
test_road_regression_9565.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// Regression tests for carla-simulator/carla#9565
8// Digital Twin Tool: crash on metropolitan OSM data, trees on roads,
9// and one-way streets excluded from generation.
10
11#include "test.h"
12
13#include <carla/geom/Vector3D.h>
15#include <carla/road/Map.h>
16
17#include <cmath>
18#include <string>
19#include <utility>
20#include <vector>
21
22
23using namespace carla::opendrive;
24using namespace carla::geom;
25
26constexpr float TREE_Y_TOLERANCE = 0.01f;
27
28// --- minimal OpenDRIVE helpers -----------------------------------------------
29
30static std::string MakeHeader() {
31 return R"(<?xml version="1.0" encoding="UTF-8"?>
32<OpenDRIVE>
33 <header revMajor="1" revMinor="4" name="" version="1"
34 north="100" south="-100" east="100" west="-100"/>
35)";
36}
37
38// Straight road running along X axis from (x0,0) to (x0+length,0).
39// lane_xml should be the full <lanes>...</lanes> block.
40static std::string MakeRoad(
41 int id, float x0, float length,
42 const std::string& lane_xml,
43 bool with_speed = true) {
44 std::string speed_block = with_speed
45 ? R"(<type s="0" type="town"><speed max="50" unit="km/h"/></type>)"
46 : "";
47 return
48 " <road name=\"R" + std::to_string(id) + "\" length=\"" + std::to_string(length) +
49 "\" id=\"" + std::to_string(id) + "\" junction=\"-1\">\n" +
50 " " + speed_block + "\n" +
51 " <planView><geometry s=\"0\" x=\"" + std::to_string(x0) +
52 "\" y=\"0\" hdg=\"0\" length=\"" + std::to_string(length) + "\"><line/></geometry></planView>\n" +
53 " <elevationProfile><elevation s=\"0\" a=\"0\" b=\"0\" c=\"0\" d=\"0\"/></elevationProfile>\n" +
54 " <lateralProfile/>\n" +
55 lane_xml + "\n" +
56 " </road>\n";
57}
58
59// Bidirectional road: lane -1 (right driving) + lane +1 (left driving).
60static std::string BidirectionalLanes() {
61 return R"(
62 <lanes>
63 <laneSection s="0">
64 <left>
65 <lane id="1" type="driving" level="false">
66 <width sOffset="0" a="3.5" b="0" c="0" d="0"/>
67 </lane>
68 </left>
69 <center><lane id="0" type="none" level="false"/></center>
70 <right>
71 <lane id="-1" type="driving" level="false">
72 <width sOffset="0" a="3.5" b="0" c="0" d="0"/>
73 </lane>
74 </right>
75 </laneSection>
76 </lanes>)";
77}
78
79// One-way road with ONLY positive lanes --no lane -1.
80// This road was silently excluded by FilterRoadsByPosition before the fix.
81static std::string PositiveOnlyLanes() {
82 return R"(
83 <lanes>
84 <laneSection s="0">
85 <left>
86 <lane id="1" type="driving" level="false">
87 <width sOffset="0" a="3.5" b="0" c="0" d="0"/>
88 </lane>
89 <lane id="2" type="driving" level="false">
90 <width sOffset="0" a="3.5" b="0" c="0" d="0"/>
91 </lane>
92 </left>
93 <center><lane id="0" type="none" level="false"/></center>
94 </laneSection>
95 </lanes>)";
96}
97
98// One-way road with ONLY negative lanes (normal right-side traffic).
99static std::string NegativeOnlyLanes() {
100 return R"(
101 <lanes>
102 <laneSection s="0">
103 <center><lane id="0" type="none" level="false"/></center>
104 <right>
105 <lane id="-1" type="driving" level="false">
106 <width sOffset="0" a="3.5" b="0" c="0" d="0"/>
107 </lane>
108 </right>
109 </laneSection>
110 </lanes>)";
111}
112
113// --- Bug 3: FilterRoadsByPosition includes one-way positive-lane roads -------
114//
115// Before fix: only queried lane -1; roads without it were silently excluded.
116// After fix: falls back to the driving lane with smallest abs(id).
117// -----------------------------------------------------------------------------
118TEST(road, filter_roads_includes_positive_only_lanes_9565) {
119 // Three roads side-by-side along X axis, all centred around Y=0.
120 // Road 0 (x=0..50): bidirectional -- has lane -1, was always included
121 // Road 1 (x=60..110): positive-only -- no lane -1, was EXCLUDED before fix
122 // Road 2 (x=120..170): negative-only -- has lane -1, was always included
123 std::string xodr = MakeHeader()
124 + MakeRoad(0, 0.f, 50.f, BidirectionalLanes())
125 + MakeRoad(1, 60.f, 50.f, PositiveOnlyLanes())
126 + MakeRoad(2, 120.f, 50.f, NegativeOnlyLanes())
127 + "</OpenDRIVE>\n";
128
129 auto map = OpenDriveParser::Load(xodr);
130 ASSERT_TRUE(map.has_value());
131
132 // Bounding box covering all three roads.
133 // FilterRoadsByPosition checks minpos.y > y > maxpos.y (Y axis is inverted),
134 // so minpos.y must be the larger (positive) value.
135 const Vector3D minpos(-10.f, 20.f, -10.f);
136 const Vector3D maxpos(200.f, -20.f, 10.f);
137
138 auto included = map->FilterRoadsByPosition(minpos, maxpos);
139
140 // All three roads must be included after the fix.
141 EXPECT_EQ(included.size(), 3u)
142 << "FilterRoadsByPosition excluded a road with positive-only lanes";
143}
144
145// --- Bug 1 + Bug 2: GetTreesTransform robustness -----------------------------
146//
147// Bug 1: roadinfo==nullptr (no maxspeed tag) caused SIGSEGV.
148// Bug 2: min_lane==0 / degenerate corners placed trees on the road surface.
149// -----------------------------------------------------------------------------
150TEST(road, get_trees_transform_no_crash_without_speed_9565) {
151 // Road without any <speed> element --roadinfo will be nullptr.
152 std::string xodr = MakeHeader()
153 + MakeRoad(0, 0.f, 100.f, BidirectionalLanes(), /*with_speed=*/false)
154 + "</OpenDRIVE>\n";
155
156 auto map = OpenDriveParser::Load(xodr);
157 ASSERT_TRUE(map.has_value());
158
159 // FilterRoadsByPosition checks minpos.y > y > maxpos.y (Y axis inverted).
160 const Vector3D minpos(-10.f, 20.f, -10.f);
161 const Vector3D maxpos(110.f, -20.f, 10.f);
162
163 // Must not crash (a regression to the null dereference will fail by crashing).
164 std::vector<std::pair<carla::geom::Transform, std::string>> result =
165 map->GetTreesTransform(minpos, maxpos, 10.f, 2.f);
166
167 // Must produce at least one tree so the fallback-type loop below is not vacuous.
168 ASSERT_GT(result.size(), 0u)
169 << "No trees generated for road without maxspeed tag";
170
171 // Fallback type "Town" must be used for all entries.
172 for (const auto& entry : result) {
173 EXPECT_EQ(entry.second, "Town")
174 << "Expected \"Town\" fallback for road without maxspeed tag";
175 }
176}
177
178TEST(road, get_trees_transform_positive_lane_road_gets_trees_9565) {
179 // One-way road with positive-only lanes: before the fix, min_lane stayed 0
180 // and the lane section was skipped --no trees were generated at all.
181 std::string xodr = MakeHeader()
182 + MakeRoad(0, 0.f, 100.f, PositiveOnlyLanes())
183 + "</OpenDRIVE>\n";
184
185 auto map = OpenDriveParser::Load(xodr);
186 ASSERT_TRUE(map.has_value());
187
188 // FilterRoadsByPosition checks minpos.y > y > maxpos.y (Y axis inverted).
189 const Vector3D minpos(-10.f, 20.f, -10.f);
190 const Vector3D maxpos(110.f, -20.f, 10.f);
191
192 auto result = map->GetTreesTransform(minpos, maxpos, 10.f, 2.f);
193
194 ASSERT_GT(result.size(), 0u)
195 << "No trees generated for one-way road with positive-only lanes";
196
197 // PositiveOnlyLanes: lane id=1 (3.5m wide) and lane id=2 (3.5m wide).
198 // Outer edge of lane id=2 is at |Y| = 3.5 + 3.5 = 7.0m from road centre.
199 // distancefromdrivinglineborder=2.0 => trees placed 2.0m beyond that edge.
200 // Trees must satisfy |Y| >= 7.0 + 2.0 = 9.0.
201 const float lane1_width = 3.5f;
202 const float lane2_width = 3.5f;
203 const float dist_from_edge = 2.0f;
204 const float outer_road_edge = lane1_width + lane2_width;
205 const float min_expected_dist = outer_road_edge + dist_from_edge; // 9.0
206 for (const auto& entry : result) {
207 const float tree_y = std::abs(entry.first.location.y);
208 EXPECT_NEAR(tree_y, min_expected_dist, TREE_Y_TOLERANCE)
209 << "Tree Y=" << entry.first.location.y
210 << " is not within ±" << TREE_Y_TOLERANCE << " of the expected minimum distance (|Y| = " << min_expected_dist << ")";
211 }
212}
213
214TEST(road, get_trees_transform_trees_outside_road_9565) {
215 // Verify trees are not placed inside the driving lane.
216 // Road: single right lane (id=-1, width=3.5m) centred at Y=0.
217 // Driving surface occupies Y in [-3.5, 0] (right side, post-Y-flip).
218 // DistanceFromRoadEdge=2.0 => trees should be at Y < -3.5 (beyond outer edge).
219 std::string xodr = MakeHeader()
220 + MakeRoad(0, 0.f, 100.f, NegativeOnlyLanes())
221 + "</OpenDRIVE>\n";
222
223 auto map = OpenDriveParser::Load(xodr);
224 ASSERT_TRUE(map.has_value());
225
226 // FilterRoadsByPosition checks minpos.y > y > maxpos.y (Y axis inverted).
227 const Vector3D minpos(-10.f, 20.f, -10.f);
228 const Vector3D maxpos(110.f, -20.f, 10.f);
229
230 auto result = map->GetTreesTransform(minpos, maxpos, 10.f, 2.f);
231 ASSERT_GT(result.size(), 0u) << "No trees generated for negative-only lane road";
232
233 // Outer edge of lane -1 is at t=3.5m from road centre.
234 // distancefromdrivinglineborder=2.0 => trees placed 2.0m beyond that edge.
235 // After Y-flip, trees must be at |Y| >= 3.5 + 2.0 = 5.5.
236 const float lane_outer_edge = 3.5f;
237 const float dist_from_edge = 2.0f;
238 const float min_expected_dist = lane_outer_edge + dist_from_edge;
239
240 for (const auto& entry : result) {
241 const float tree_y = std::abs(entry.first.location.y);
242 EXPECT_NEAR(tree_y, min_expected_dist, TREE_Y_TOLERANCE)
243 << "Tree Y=" << entry.first.location.y
244 << " is not within ±" << TREE_Y_TOLERANCE << " of the expected minimum distance (|Y| = " << min_expected_dist << ")";
245 }
246}
std::map< std::string, unsigned int > road
static std::optional< road::Map > Load(const std::string &opendrive)
constexpr float TREE_Y_TOLERANCE
static std::string BidirectionalLanes()
static std::string MakeHeader()
TEST(road, filter_roads_includes_positive_only_lanes_9565)
static std::string NegativeOnlyLanes()
static std::string PositiveOnlyLanes()
static std::string MakeRoad(int id, float x0, float length, const std::string &lane_xml, bool with_speed=true)