CARLA
Loading...
Searching...
No Matches
XodrExport.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 "XodrExport.h"
8
9#include "PlanView.h"
10
11#include <algorithm>
12#include <cmath>
13#include <string>
14#include <ctime>
15#include <map>
16#include <set>
17#include <sstream>
18
20
21namespace twinmodel {
22
23 namespace {
24
25
26 /// Deterministic integer ids: a numeric model id >= 1 is kept when it is
27 /// free, anything else takes the next unused one. Roads are allocated
28 /// first and junctions from the same pool, so the two never collide.
29 struct IdMap {
30 std::map<std::string, unsigned int> road;
31 std::map<std::string, unsigned int> junction;
32
33 const char *RoadOrMinusOne(const std::optional<std::string> &id, std::string &scratch) const {
34 if (!id.has_value()) {
35 return "-1";
36 }
37 const auto it = junction.find(*id);
38 if (it == junction.end()) {
39 return "-1";
40 }
41 scratch = std::to_string(it->second);
42 return scratch.c_str();
43 }
44 };
45
46 void Allocate(const std::vector<std::string> &ids,
47 std::set<unsigned int> &used,
48 std::map<std::string, unsigned int> &out) {
49 std::vector<std::string> pending;
50 for (const std::string &id : ids) {
51 const bool numeric = !id.empty() &&
52 id.find_first_not_of("0123456789") == std::string::npos;
53 if (numeric) {
54 const unsigned long value = std::stoul(id);
55 if (value >= 1ul && used.count(static_cast<unsigned int>(value)) == 0u) {
56 out[id] = static_cast<unsigned int>(value);
57 used.insert(static_cast<unsigned int>(value));
58 continue;
59 }
60 }
61 pending.push_back(id);
62 }
63 unsigned int next = used.empty() ? 1u : (*used.rbegin() + 1u);
64 for (const std::string &id : pending) {
65 while (used.count(next) != 0u) {
66 ++next;
67 }
68 out[id] = next;
69 used.insert(next);
70 }
71 }
72
73 IdMap BuildIdMap(const TwinModelData &model) {
74 IdMap map;
75 std::set<unsigned int> used;
76 std::vector<std::string> road_ids;
77 for (const Road &road : model.roads) {
78 road_ids.push_back(road.id);
79 }
80 Allocate(road_ids, used, map.road);
81 std::vector<std::string> junction_ids;
82 for (const Junction &junction : model.junctions) {
83 junction_ids.push_back(junction.id);
84 }
85 Allocate(junction_ids, used, map.junction);
86 return map;
87 }
88
89 std::string Format(double value) {
90 std::ostringstream out;
91 out.precision(10);
92 out << value;
93 return out.str();
94 }
95
96 std::string Timestamp() {
97 const std::time_t now = std::time(nullptr);
98 std::tm utc{};
99#if defined(_WIN32)
100 gmtime_s(&utc, &now);
101#else
102 gmtime_r(&now, &utc);
103#endif
104 char buffer[32] = {0};
105 std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S", &utc);
106 return buffer;
107 }
108
109 void SetAttribute(pugi::xml_node node, const char *name, double value) {
110 node.append_attribute(name).set_value(Format(value).c_str());
111 }
112
113 /// Lanes in OpenDRIVE order: left outward ascending, then centre, then
114 /// right outward descending.
115 void WriteLaneSection(pugi::xml_node lanes_node, const Road &road) {
116 pugi::xml_node section = lanes_node.append_child("laneSection");
117 SetAttribute(section, "s", 0.0);
118
119 std::vector<const Lane *> left;
120 std::vector<const Lane *> right;
121 for (const Lane &lane : road.lanes) {
122 if (lane.id > 0) {
123 left.push_back(&lane);
124 } else if (lane.id < 0) {
125 right.push_back(&lane);
126 }
127 }
128 std::sort(left.begin(), left.end(), [](const Lane *a, const Lane *b) {
129 return a->id < b->id;
130 });
131 std::sort(right.begin(), right.end(), [](const Lane *a, const Lane *b) {
132 return a->id > b->id;
133 });
134
135 auto write_lane = [&](pugi::xml_node parent, const Lane &lane) {
136 pugi::xml_node node = parent.append_child("lane");
137 node.append_attribute("id").set_value(lane.id);
138 node.append_attribute("type").set_value(ToString(lane.type));
139 node.append_attribute("level").set_value("false");
140 if (lane.predecessor_lane.has_value() || lane.successor_lane.has_value()) {
141 pugi::xml_node link = node.append_child("link");
142 if (lane.predecessor_lane.has_value()) {
143 link.append_child("predecessor").append_attribute("id")
144 .set_value(*lane.predecessor_lane);
145 }
146 if (lane.successor_lane.has_value()) {
147 link.append_child("successor").append_attribute("id")
148 .set_value(*lane.successor_lane);
149 }
150 }
151 pugi::xml_node width = node.append_child("width");
152 SetAttribute(width, "sOffset", 0.0);
153 SetAttribute(width, "a", lane.width);
154 SetAttribute(width, "b", 0.0);
155 SetAttribute(width, "c", 0.0);
156 SetAttribute(width, "d", 0.0);
157 if (lane.marking.has_value()) {
158 pugi::xml_node mark = node.append_child("roadMark");
159 SetAttribute(mark, "sOffset", 0.0);
160 mark.append_attribute("type").set_value(ToString(lane.marking->kind));
161 mark.append_attribute("color").set_value(ToString(lane.marking->color));
162 SetAttribute(mark, "width", lane.marking->width);
163 mark.append_attribute("laneChange").set_value("both");
164 }
165 if (lane.speed_limit.has_value()) {
166 pugi::xml_node speed = node.append_child("speed");
167 SetAttribute(speed, "sOffset", 0.0);
168 SetAttribute(speed, "max", *lane.speed_limit);
169 speed.append_attribute("unit").set_value("m/s");
170 }
171 };
172
173 if (!left.empty()) {
174 pugi::xml_node node = section.append_child("left");
175 for (const Lane *lane : left) {
176 write_lane(node, *lane);
177 }
178 }
179
180 pugi::xml_node centre = section.append_child("center");
181 pugi::xml_node centre_lane = centre.append_child("lane");
182 centre_lane.append_attribute("id").set_value(0);
183 centre_lane.append_attribute("type").set_value("none");
184 centre_lane.append_attribute("level").set_value("false");
185 if (road.center_marking.has_value()) {
186 pugi::xml_node mark = centre_lane.append_child("roadMark");
187 SetAttribute(mark, "sOffset", 0.0);
188 mark.append_attribute("type").set_value(ToString(road.center_marking->kind));
189 mark.append_attribute("color").set_value(ToString(road.center_marking->color));
190 SetAttribute(mark, "width", road.center_marking->width);
191 mark.append_attribute("laneChange").set_value("none");
192 }
193
194 if (!right.empty()) {
195 pugi::xml_node node = section.append_child("right");
196 for (const Lane *lane : right) {
197 write_lane(node, *lane);
198 }
199 }
200 }
201
202 void WriteLink(pugi::xml_node road_node, const Road &road, const IdMap &ids) {
203 if (!road.predecessor.has_value() && !road.successor.has_value()) {
204 return;
205 }
206 pugi::xml_node link = road_node.append_child("link");
207 auto write = [&](const char *name, const RoadLink &value) {
208 pugi::xml_node node = link.append_child(name);
209 node.append_attribute("elementType").set_value(
210 value.element == LinkElement::Road ? "road" : "junction");
211 const auto &pool = (value.element == LinkElement::Road) ? ids.road : ids.junction;
212 const auto it = pool.find(value.id);
213 node.append_attribute("elementId").set_value(it == pool.end() ? 0u : it->second);
214 if (value.contact.has_value()) {
215 node.append_attribute("contactPoint").set_value(ToString(*value.contact));
216 }
217 };
218 if (road.predecessor.has_value()) {
219 write("predecessor", *road.predecessor);
220 }
221 if (road.successor.has_value()) {
222 write("successor", *road.successor);
223 }
224 }
225
226 /// Piecewise cubic planview; a straight leg between two locked corners is
227 /// written as a <line> instead.
228 double WritePlanView(pugi::xml_node road_node, const Road &road) {
229 pugi::xml_node plan = road_node.append_child("planView");
230 double s = 0.0;
231 for (const Geom &fitted : FitPlanView(road.reference_line)) {
232 pugi::xml_node geometry = plan.append_child("geometry");
233 SetAttribute(geometry, "s", fitted.s);
234 SetAttribute(geometry, "x", fitted.x);
235 SetAttribute(geometry, "y", fitted.y);
236 SetAttribute(geometry, "hdg", fitted.hdg);
237 SetAttribute(geometry, "length", fitted.length);
238 if (fitted.is_line) {
239 geometry.append_child("line");
240 } else {
241 pugi::xml_node poly = geometry.append_child("paramPoly3");
242 static const char *names[8] = {"aU", "bU", "cU", "dU", "aV", "bV", "cV", "dV"};
243 for (int i = 0; i < 8; ++i) {
244 SetAttribute(poly, names[i], fitted.coeffs[static_cast<std::size_t>(i)]);
245 }
246 poly.append_attribute("pRange").set_value("normalized");
247 }
248 s = fitted.s + fitted.length;
249 }
250 return s;
251 }
252
253 void WriteElevation(pugi::xml_node road_node, const Road &road) {
254 pugi::xml_node profile = road_node.append_child("elevationProfile");
255 pugi::xml_node elevation = profile.append_child("elevation");
256 SetAttribute(elevation, "s", 0.0);
257 SetAttribute(elevation, "a", road.reference_line.empty() ? 0.0 : road.reference_line.front().z);
258 SetAttribute(elevation, "b", 0.0);
259 SetAttribute(elevation, "c", 0.0);
260 SetAttribute(elevation, "d", 0.0);
261 }
262
263
264 /// <signals> on a road; a crosswalk is not a signal in CARLA but an
265 /// <object type="crosswalk"> with a <cornerLocal> outline.
266 void WriteSignalsAndObjects(pugi::xml_node road_node, const Road &road,
267 const TwinModelData &model, const IdMap &ids,
268 double crosswalk_half) {
269 std::vector<const Signal *> signals;
270 std::vector<const Signal *> crossings;
271 for (const Signal &signal : model.signals) {
272 if (signal.road_id != road.id) {
273 continue;
274 }
275 (signal.kind == SignalKind::Crosswalk ? crossings : signals).push_back(&signal);
276 }
277
278 if (!signals.empty()) {
279 pugi::xml_node node = road_node.append_child("signals");
280 unsigned int index = 0u;
281 for (const Signal *signal : signals) {
282 const SignalType type = GetSignalType(signal->kind);
283 pugi::xml_node element = node.append_child("signal");
284 SetAttribute(element, "s", signal->s);
285 SetAttribute(element, "t", signal->t);
286 element.append_attribute("id").set_value(index++);
287 element.append_attribute("name").set_value(signal->id.c_str());
288 element.append_attribute("dynamic").set_value(type.dynamic ? "yes" : "no");
289 element.append_attribute("orientation").set_value(signal->orientation.c_str());
290 SetAttribute(element, "zOffset", 0.0);
291 element.append_attribute("type").set_value(type.type);
292 if (signal->kind == SignalKind::SpeedLimit && signal->value.has_value()) {
293 // CARLA synthesises type 274 with the km/h value as the subtype
294 const long kmh = std::lround(*signal->value * 3.6);
295 element.append_attribute("subtype").set_value(std::to_string(kmh).c_str());
296 SetAttribute(element, "value", static_cast<double>(kmh));
297 element.append_attribute("unit").set_value("km/h");
298 } else {
299 element.append_attribute("subtype").set_value(type.subtype);
300 }
301 element.append_attribute("country").set_value("OpenDRIVE");
302 SetAttribute(element, "hOffset", signal->h_offset);
303 SetAttribute(element, "pitch", 0.0);
304 SetAttribute(element, "roll", 0.0);
305 }
306 }
307
308 if (!crossings.empty()) {
309 pugi::xml_node node = road_node.append_child("objects");
310 unsigned int index = 0u;
311 for (const Signal *crossing : crossings) {
312 pugi::xml_node element = node.append_child("object");
313 SetAttribute(element, "s", crossing->s);
314 SetAttribute(element, "t", 0.0);
315 element.append_attribute("id").set_value(index++);
316 element.append_attribute("name").set_value("crosswalk");
317 element.append_attribute("type").set_value("crosswalk");
318 SetAttribute(element, "zOffset", 0.0);
319 SetAttribute(element, "hdg", 0.0);
320 SetAttribute(element, "width", crosswalk_half * 2.0);
321 // half the carriageway each side, so the outline spans the lanes
322 double half = 0.0;
323 for (const Lane &lane : road.lanes) {
324 half = std::max(half, std::abs(static_cast<double>(lane.id)) * lane.width);
325 }
326 half = std::max(half, 3.5);
327 SetAttribute(element, "length", half * 2.0);
328 pugi::xml_node outline = element.append_child("outline");
329 const double corners[4][2] = {{-crosswalk_half, -half}, {crosswalk_half, -half},
330 {crosswalk_half, half}, {-crosswalk_half, half}};
331 for (const auto &corner : corners) {
332 pugi::xml_node point = outline.append_child("cornerLocal");
333 SetAttribute(point, "u", corner[0]);
334 SetAttribute(point, "v", corner[1]);
335 SetAttribute(point, "z", 0.0);
336 SetAttribute(point, "height", 0.0);
337 }
338 }
339 }
340 (void) ids;
341 }
342
343 } // namespace
344
345 std::string ExportXodr(const TwinModelData &model, const LocalFrame &frame) {
346 pugi::xml_document document;
348 declaration.append_attribute("version").set_value("1.0");
349 declaration.append_attribute("encoding").set_value("UTF-8");
350
351 pugi::xml_node root = document.append_child("OpenDRIVE");
352 pugi::xml_node header = root.append_child("header");
353 header.append_attribute("revMajor").set_value("1");
354 header.append_attribute("revMinor").set_value("6");
355 header.append_attribute("name").set_value(model.name.c_str());
356 header.append_attribute("version").set_value("1.00");
357 header.append_attribute("date").set_value(Timestamp().c_str());
358 header.append_attribute("vendor").set_value("twinmodel");
359
360 pugi::xml_node geo = header.append_child("geoReference");
362
363 const IdMap ids = BuildIdMap(model);
364
365 for (const Road &road : model.roads) {
366 pugi::xml_node node = root.append_child("road");
367 node.append_attribute("name").set_value(road.name.c_str());
368 node.append_attribute("id").set_value(ids.road.at(road.id));
369 std::string scratch;
370 node.append_attribute("junction").set_value(ids.RoadOrMinusOne(road.junction_id, scratch));
371 node.append_attribute("rule").set_value("RHT");
372 WriteLink(node, road, ids);
373 const double length = WritePlanView(node, road);
374 node.insert_attribute_after("length", node.attribute("name")).set_value(Format(length).c_str());
375 WriteElevation(node, road);
376 pugi::xml_node lanes = node.append_child("lanes");
377 WriteLaneSection(lanes, road);
378 WriteSignalsAndObjects(node, road, model, ids, 2.0);
379 }
380
381 for (const Junction &junction : model.junctions) {
382 pugi::xml_node node = root.append_child("junction");
383 node.append_attribute("name").set_value(junction.name.c_str());
384 node.append_attribute("id").set_value(ids.junction.at(junction.id));
385 unsigned int connection_index = 0u;
386 for (const Connection &connection : junction.connections) {
387 const auto incoming = ids.road.find(connection.incoming_road);
388 const auto connecting = ids.road.find(connection.connecting_road);
389 if (incoming == ids.road.end() || connecting == ids.road.end()) {
390 continue;
391 }
392 pugi::xml_node entry = node.append_child("connection");
393 entry.append_attribute("id").set_value(connection_index++);
394 entry.append_attribute("incomingRoad").set_value(incoming->second);
395 entry.append_attribute("connectingRoad").set_value(connecting->second);
396 entry.append_attribute("contactPoint").set_value(ToString(connection.contact_point));
397 for (const LaneLink &link : connection.lane_links) {
398 pugi::xml_node lane = entry.append_child("laneLink");
399 lane.append_attribute("from").set_value(link.from_lane);
400 lane.append_attribute("to").set_value(link.to_lane);
401 }
402 }
403 }
404
405 unsigned int controller_index = 0u;
406 for (const Controller &controller : model.controllers) {
407 pugi::xml_node node = root.append_child("controller");
408 node.append_attribute("id").set_value(controller_index++);
409 node.append_attribute("name").set_value(controller.id.c_str());
410 node.append_attribute("sequence").set_value(controller.sequence);
411 for (const std::string &signal_id : controller.signal_ids) {
412 unsigned int signal_index = 0u;
413 for (const Signal &signal : model.signals) {
414 if (signal.id == signal_id) {
415 pugi::xml_node control = node.append_child("control");
416 control.append_attribute("signalId").set_value(signal_index);
417 control.append_attribute("type").set_value("");
418 break;
419 }
420 ++signal_index;
421 }
422 }
423 }
424
425 std::ostringstream out;
426 document.save(out, " ");
427 return out.str();
428 }
429
430} // namespace twinmodel
float elevation
float z
long long node
Definition LaneGraph.cpp:43
geom::Point point
Definition LaneGraph.cpp:41
std::map< std::string, unsigned int > road
std::map< std::string, unsigned int > junction
bool set_value(const char_t *rhs)
Definition pugixml.cpp:5299
void save(xml_writer &writer, const char_t *indent=PUGIXML_TEXT("\t"), unsigned int flags=format_default, xml_encoding encoding=encoding_auto) const
Definition pugixml.cpp:7149
bool set_value(const char_t *rhs)
Definition pugixml.cpp:5642
xml_node append_child(xml_node_type type=node_element)
Definition pugixml.cpp:5792
xml_attribute append_attribute(const char_t *name)
Definition pugixml.cpp:5652
xml_node prepend_child(xml_node_type type=node_element)
Definition pugixml.cpp:5809
WGS84 <-> local ENU metres, a transverse Mercator centred on the origin.
Definition Frame.h:15
std::string GetProjString() const
The +proj string written as the OpenDRIVE <geoReference>.
Definition Frame.cpp:26
@ node_declaration
Definition pugixml.hpp:149
@ node_cdata
Definition pugixml.hpp:146
std::string ExportXodr(const TwinModelData &model, const LocalFrame &frame)
Serialise the model as an OpenDRIVE 1.6 document.
std::vector< Geom > FitPlanView(const geom::Line3 &line, double max_offset)
Fit a polyline as a piecewise cubic paramPoly3 planview.
Definition PlanView.cpp:102
SignalType GetSignalType(SignalKind kind)
const char * ToString(LaneType type)
Everything the OpenDRIVE writer reads.
Definition Model.h:158
std::vector< Signal > signals
Definition Model.h:164
std::vector< Road > roads
Definition Model.h:162
std::vector< Controller > controllers
Definition Model.h:165
std::vector< Junction > junctions
Definition Model.h:163