CARLA
Loading...
Searching...
No Matches
LaneGraph.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 "LaneGraph.h"
8
9#include <algorithm>
10#include <limits>
11#include <map>
12#include <set>
13#include <sstream>
14#include <tuple>
15
16#include "Profiles.h"
17
18namespace twinmodel {
19
20 namespace {
21
22 struct WaySegment {
23 long long way_id = 0;
24 std::string highway;
26 std::vector<long long> nodes;
27 };
28
29 /// Endpoint of a road at a junction, with the heading pointing away from
30 /// the junction; the connecting roads are built by pairing these up.
31 /// A road built from one stretch of a way, with the OSM nodes its ends sit
32 /// on so stretches of consecutive ways can be chained into one road.
33 struct PieceEnds {
34 long long first = 0;
35 long long last = 0;
36 };
37
38 struct JunctionLeg {
39 std::string road_id;
42 double heading = 0.0;
43 long long node = 0;
44 };
45
46
47 /// Disjoint sets over OSM node ids, for the junction clustering.
48 class UnionFind {
49 public:
50
51 void Add(long long id) {
52 _parent.emplace(id, id);
53 }
54
55 long long Find(long long id) {
56 auto it = _parent.find(id);
57 if (it == _parent.end()) {
58 _parent.emplace(id, id);
59 return id;
60 }
61 while (it->second != id) {
62 id = it->second;
63 it = _parent.find(id);
64 }
65 return id;
66 }
67
68 void Join(long long a, long long b) {
69 const long long ra = Find(a);
70 const long long rb = Find(b);
71 if (ra != rb) {
72 _parent[ra] = rb;
73 }
74 }
75
76 private:
77
78 std::map<long long, long long> _parent;
79 };
80
81 std::string MakeId(const char *prefix, std::size_t index) {
82 std::ostringstream out;
83 out << prefix << index;
84 return out.str();
85 }
86
87 /// Lanes outward from the reference line: negative ids run forward, the
88 /// positive ones backward, which is OpenDRIVE's convention for RHT.
89 void FillLanes(Road &road, const LaneProfile &profile) {
90 int id = -1;
91 for (int i = 0; i < profile.forward_lanes; ++i, --id) {
92 Lane lane;
93 lane.id = id;
94 lane.type = LaneType::Driving;
95 lane.width = profile.lane_width;
96 lane.direction = Direction::Forward;
97 lane.speed_limit = profile.speed_limit;
98 road.lanes.push_back(lane);
99 }
100 if (profile.shoulder.has_value() && profile.forward_lanes > 0) {
101 Lane lane;
102 lane.id = id--;
103 lane.type = LaneType::Shoulder;
104 lane.width = *profile.shoulder;
105 lane.direction = Direction::Forward;
106 road.lanes.push_back(lane);
107 }
108 if (profile.sidewalk_width.has_value() && profile.forward_lanes > 0) {
109 Lane lane;
110 lane.id = id--;
111 lane.type = LaneType::Sidewalk;
112 lane.width = *profile.sidewalk_width;
113 lane.direction = Direction::Both;
114 road.lanes.push_back(lane);
115 }
116
117 id = 1;
118 for (int i = 0; i < profile.backward_lanes; ++i, ++id) {
119 Lane lane;
120 lane.id = id;
121 lane.type = LaneType::Driving;
122 lane.width = profile.lane_width;
123 lane.direction = Direction::Backward;
124 lane.speed_limit = profile.speed_limit;
125 road.lanes.push_back(lane);
126 }
127 if (profile.shoulder.has_value() && profile.backward_lanes > 0) {
128 Lane lane;
129 lane.id = id++;
130 lane.type = LaneType::Shoulder;
131 lane.width = *profile.shoulder;
132 lane.direction = Direction::Backward;
133 road.lanes.push_back(lane);
134 }
135 if (profile.sidewalk_width.has_value() && profile.backward_lanes > 0) {
136 Lane lane;
137 lane.id = id++;
138 lane.type = LaneType::Sidewalk;
139 lane.width = *profile.sidewalk_width;
140 lane.direction = Direction::Both;
141 road.lanes.push_back(lane);
142 }
143
144 if (profile.backward_lanes > 0 && profile.forward_lanes > 0) {
145 Marking centre;
146 centre.kind = MarkingKind::Broken;
147 centre.color = MarkingColor::White;
148 road.center_marking = centre;
149 }
150 }
151
152
153 /// Lanes of @a road that arrive at the junction touching the given end,
154 /// and the lanes that leave from it. At the start of a road the backward
155 /// lanes (positive ids) arrive and the forward ones (negative) leave.
156 void SplitLanesAtContact(const Road &road, ContactPoint contact,
157 std::vector<int> &arriving, std::vector<int> &leaving) {
158 for (const Lane &lane : road.lanes) {
159 if (lane.type != LaneType::Driving) {
160 continue;
161 }
162 const bool forward = lane.id < 0;
163 const bool arrives = (contact == ContactPoint::Start) ? !forward : forward;
164 (arrives ? arriving : leaving).push_back(lane.id);
165 }
166 }
167
168 /// Cubic Hermite from @a from (heading into the junction) to @a to
169 /// (heading out of it), sampled about every metre.
170 geom::Line3 ConnectingGeometry(const geom::Point &from, double heading_in,
171 const geom::Point &to, double heading_out, double z) {
172 const double dx = geom::bg::get<0>(to) - geom::bg::get<0>(from);
173 const double dy = geom::bg::get<1>(to) - geom::bg::get<1>(from);
174 const double span = std::max(1.0, std::hypot(dx, dy));
175 const double tangent = span * 0.65;
176 const double x0 = geom::bg::get<0>(from);
177 const double y0 = geom::bg::get<1>(from);
178 const double x1 = geom::bg::get<0>(to);
179 const double y1 = geom::bg::get<1>(to);
180 const double mx0 = std::cos(heading_in) * tangent;
181 const double my0 = std::sin(heading_in) * tangent;
182 const double mx1 = std::cos(heading_out) * tangent;
183 const double my1 = std::sin(heading_out) * tangent;
184
185 const int steps = std::max(4, static_cast<int>(span));
186 geom::Line3 line;
187 line.reserve(static_cast<std::size_t>(steps) + 1u);
188 for (int i = 0; i <= steps; ++i) {
189 const double t = static_cast<double>(i) / static_cast<double>(steps);
190 const double t2 = t * t;
191 const double t3 = t2 * t;
192 line.push_back(geom::Point3{
193 (2.0 * t3 - 3.0 * t2 + 1.0) * x0 + (t3 - 2.0 * t2 + t) * mx0 +
194 (-2.0 * t3 + 3.0 * t2) * x1 + (t3 - t2) * mx1,
195 (2.0 * t3 - 3.0 * t2 + 1.0) * y0 + (t3 - 2.0 * t2 + t) * my0 +
196 (-2.0 * t3 + 3.0 * t2) * y1 + (t3 - t2) * my1,
197 z});
198 }
199 return line;
200 }
201
202 double LineLength(const geom::Line3 &line) {
203 double total = 0.0;
204 for (std::size_t i = 1u; i < line.size(); ++i) {
205 total += std::hypot(line[i].x - line[i - 1u].x, line[i].y - line[i - 1u].y);
206 }
207 return total;
208 }
209
210 /// Trim @a length metres off the given end of a reference line, leaving the
211 /// junction box free for the connecting roads.
212 void TrimEnd(geom::Line3 &line, double length, bool from_start) {
213 if (line.size() < 2u || length <= 0.0) {
214 return;
215 }
216 const double total = LineLength(line);
217 if (total <= length * 2.0 + 0.5) {
218 return;
219 }
220 geom::LineString flat = geom::To2D(line);
221 const double from = from_start ? length : 0.0;
222 const double to = from_start ? total : total - length;
223 const geom::LineString cut = geom::Substring(flat, from, to);
224 geom::Line3 trimmed;
225 trimmed.reserve(cut.size());
226 const double z = line.front().z;
227 for (const auto &p : cut) {
228 trimmed.push_back(geom::Point3{geom::bg::get<0>(p), geom::bg::get<1>(p), z});
229 }
230 line = std::move(trimmed);
231 }
232
233
234 /// Keep the stretch of every road that lies inside @a box and drop the
235 /// roads that fall outside it, with the junctions left dangling removed.
236 void ClipToBox(TwinModelData &model, const geom::Box &box) {
237 auto inside = [&box](const geom::Point3 &p) {
238 return p.x >= box.min_corner().x() && p.x <= box.max_corner().x() &&
239 p.y >= box.min_corner().y() && p.y <= box.max_corner().y();
240 };
241 std::vector<Road> kept;
242 std::set<std::string> kept_ids;
243 // a connector is only useful while both the road it leaves and the one
244 // it joins survive the clip
245 std::set<std::string> survivors;
246 for (const Road &road : model.roads) {
247 // the same test the keep loop below applies: a road needs two points
248 // inside the box to leave a drivable stretch behind
249 std::size_t first = road.reference_line.size();
250 std::size_t last = 0u;
251 for (std::size_t i = 0u; i < road.reference_line.size(); ++i) {
252 if (inside(road.reference_line[i])) {
253 first = std::min(first, i);
254 last = std::max(last, i);
255 }
256 }
257 if (first < road.reference_line.size() && last > first) {
258 survivors.insert(road.id);
259 }
260 }
261 for (Road &road : model.roads) {
262 if (road.junction_id.has_value()) {
263 const bool from_ok = !road.predecessor.has_value() ||
264 survivors.count(road.predecessor->id) != 0u;
265 const bool to_ok = !road.successor.has_value() ||
266 survivors.count(road.successor->id) != 0u;
267 if (!from_ok || !to_ok) {
268 continue;
269 }
270 }
271 std::size_t first = road.reference_line.size();
272 std::size_t last = 0u;
273 for (std::size_t i = 0u; i < road.reference_line.size(); ++i) {
274 if (inside(road.reference_line[i])) {
275 first = std::min(first, i);
276 last = std::max(last, i);
277 }
278 }
279 if (first >= road.reference_line.size() || last <= first) {
280 continue;
281 }
282 if (first != 0u || last + 1u != road.reference_line.size()) {
283 geom::Line3 trimmed(road.reference_line.begin() + static_cast<long>(first),
284 road.reference_line.begin() + static_cast<long>(last) + 1);
285 road.reference_line = std::move(trimmed);
286 // a trimmed road no longer reaches whatever it used to link to
287 road.predecessor.reset();
288 road.successor.reset();
289 }
290 kept_ids.insert(road.id);
291 kept.push_back(std::move(road));
292 }
293 model.roads = std::move(kept);
294
295 std::vector<Junction> junctions;
296 for (Junction &junction : model.junctions) {
297 std::vector<Connection> connections;
298 for (Connection &connection : junction.connections) {
299 if (kept_ids.count(connection.incoming_road) != 0u &&
300 kept_ids.count(connection.connecting_road) != 0u) {
301 connections.push_back(std::move(connection));
302 }
303 }
304 if (connections.empty()) {
305 continue;
306 }
307 junction.connections = std::move(connections);
308 junctions.push_back(std::move(junction));
309 }
310 model.junctions = std::move(junctions);
311
312 // drop links that point at a road or junction the clip removed
313 std::set<std::string> junction_ids;
314 for (const Junction &junction : model.junctions) {
315 junction_ids.insert(junction.id);
316 }
317 for (Road &road : model.roads) {
318 auto prune = [&](std::optional<RoadLink> &link) {
319 if (!link.has_value()) {
320 return;
321 }
322 const bool alive = (link->element == LinkElement::Road)
323 ? kept_ids.count(link->id) != 0u
324 : junction_ids.count(link->id) != 0u;
325 if (!alive) {
326 link.reset();
327 }
328 };
329 prune(road.predecessor);
330 prune(road.successor);
331 if (road.junction_id.has_value() && junction_ids.count(*road.junction_id) == 0u) {
332 road.junction_id.reset();
333 }
334 }
335 }
336
337 } // namespace
338
340 const OsmData &osm,
341 const LocalFrame &frame,
342 const std::string &name,
343 const LaneGraphOptions &options) {
344
345 TwinModelData model;
346 model.name = name;
347 model.origin_lat = frame.GetOriginLatitude();
348 model.origin_lon = frame.GetOriginLongitude();
349
350 // 1. the drivable ways, and how often each node is used by one
351 std::vector<WaySegment> drivable;
352 std::map<long long, int> node_use;
353 for (const OsmWay &way : osm.ways) {
354 const std::string *highway = FindTag(way.tags, "highway");
355 if (highway == nullptr || !IsDrivable(*highway)) {
356 continue;
357 }
358 WaySegment segment;
359 segment.way_id = way.id;
360 segment.highway = *highway;
361 segment.tags = way.tags;
362 segment.nodes = way.nodes;
363 // degree counts incident segments, so an interior node scores 2 and a
364 // way end 1; two ways meeting end to end stay degree 2 and get chained
365 for (std::size_t i = 0u; i < way.nodes.size(); ++i) {
366 const bool interior = (i > 0u) && (i + 1u < way.nodes.size());
367 node_use[way.nodes[i]] += interior ? 2 : 1;
368 }
369 drivable.push_back(std::move(segment));
370 }
371
372 // only a node where three or more segments meet is an intersection
373 std::set<long long> junction_nodes;
374 for (const auto &entry : node_use) {
375 if (entry.second >= 3) {
376 junction_nodes.insert(entry.first);
377 }
378 }
379
380 auto node_point = [&](long long id, geom::Point &out) {
381 const auto it = osm.nodes.find(id);
382 if (it == osm.nodes.end()) {
383 return false;
384 }
385 double x = 0.0;
386 double y = 0.0;
387 frame.ToLocal(it->second.lon, it->second.lat, x, y);
388 out = geom::Point(x, y);
389 return true;
390 };
391
392 // Cluster intersection nodes: two join when the chain of degree-2 nodes
393 // between them is shorter than cluster_m and they are closer than that.
394 UnionFind clusters;
395 std::map<long long, geom::Box> extent_of;
396 for (long long node : junction_nodes) {
397 clusters.Add(node);
399 if (node_point(node, point)) {
400 extent_of[node] = geom::Box(point, point);
401 }
402 }
403 for (const WaySegment &way : drivable) {
404 long long previous = 0;
405 bool have_previous = false;
406 double chain = 0.0;
407 geom::Point last_point;
408 bool have_last = false;
409 for (long long node : way.nodes) {
411 if (!node_point(node, point)) {
412 continue;
413 }
414 if (have_last) {
415 chain += geom::Hypot(last_point, point);
416 }
417 last_point = point;
418 have_last = true;
419 if (junction_nodes.count(node) == 0u) {
420 continue;
421 }
422 if (have_previous && chain <= options.cluster_m) {
423 geom::Point previous_point;
424 if (node_point(previous, previous_point) &&
425 geom::Hypot(previous_point, point) <= options.cluster_m) {
426 // bound the merged cluster: without this a chain of short hops
427 // daisy-chains separate intersections into one junction
428 const long long ra = clusters.Find(previous);
429 const long long rb = clusters.Find(node);
430 if (ra != rb) {
431 geom::Box merged = extent_of[ra];
432 geom::bg::expand(merged, extent_of[rb]);
433 const double width = merged.max_corner().x() - merged.min_corner().x();
434 const double height = merged.max_corner().y() - merged.min_corner().y();
435 if (std::max(width, height) <= options.cluster_m * 0.75) {
436 clusters.Join(previous, node);
437 extent_of[clusters.Find(previous)] = merged;
438 }
439 }
440 }
441 }
442 previous = node;
443 have_previous = true;
444 chain = 0.0;
445 }
446 }
447
448 // 2. cut each way at its junction nodes and project to model space
449 std::map<long long, std::string> junction_of_node;
450 std::vector<JunctionLeg> legs;
451 std::map<std::string, PieceEnds> ends_of_road;
452 std::size_t road_index = 0u;
453
454 auto project = [&](long long node_id, geom::Point3 &out) {
455 const auto it = osm.nodes.find(node_id);
456 if (it == osm.nodes.end()) {
457 return false;
458 }
459 double x = 0.0;
460 double y = 0.0;
461 frame.ToLocal(it->second.lon, it->second.lat, x, y);
462 out = geom::Point3{x, y, 0.0};
463 return true;
464 };
465
466 for (const WaySegment &way : drivable) {
467 // Split where the way crosses a junction cluster, not at every shared
468 // node: a run of nodes inside one cluster is a single crossing, so the
469 // stretches between clusters stay one road.
470 std::vector<std::vector<long long>> pieces;
471 std::vector<long long> current;
472 long long open_cluster = 0;
473 bool inside_cluster = false;
474 for (std::size_t i = 0u; i < way.nodes.size(); ++i) {
475 const long long node = way.nodes[i];
476 const bool is_junction = junction_nodes.count(node) != 0u;
477 const long long cluster = is_junction ? clusters.Find(node) : 0;
478
479 if (is_junction && (!inside_cluster || cluster != open_cluster)) {
480 // leaving the previous stretch: close it on this node
481 current.push_back(node);
482 if (current.size() >= 2u) {
483 pieces.push_back(current);
484 }
485 current.clear();
486 current.push_back(node);
487 open_cluster = cluster;
488 inside_cluster = true;
489 continue;
490 }
491 if (is_junction && inside_cluster && cluster == open_cluster) {
492 // still inside the same junction: drop the intermediate node and
493 // restart the next stretch from it
494 current.clear();
495 current.push_back(node);
496 continue;
497 }
498 inside_cluster = false;
499 current.push_back(node);
500 }
501 if (current.size() >= 2u) {
502 pieces.push_back(current);
503 }
504
505 const LaneProfile profile = ResolveProfile(way.highway, way.tags);
506 for (const std::vector<long long> &piece : pieces) {
507 Road road;
508 road.id = MakeId("road_", road_index++);
509 road.highway = way.highway;
510 road.osm_way_ids.push_back(way.way_id);
511 road.tags = way.tags;
512 const std::string *way_name = FindTag(way.tags, "name");
513 if (way_name != nullptr) {
514 road.name = *way_name;
515 }
516 for (long long node : piece) {
518 if (project(node, point)) {
519 road.reference_line.push_back(point);
520 }
521 }
522 if (road.reference_line.size() < 2u ||
523 LineLength(road.reference_line) < options.min_road_length) {
524 --road_index;
525 continue;
526 }
527 FillLanes(road, profile);
528
529 // remember the ends that sit on a junction node before trimming
530 const long long first_node = piece.front();
531 const long long last_node = piece.back();
532 const bool start_on_junction = junction_nodes.count(first_node) != 0u;
533 const bool end_on_junction = junction_nodes.count(last_node) != 0u;
534
535 if (start_on_junction) {
536 TrimEnd(road.reference_line, options.junction_radius, true);
537 }
538 if (end_on_junction) {
539 TrimEnd(road.reference_line, options.junction_radius, false);
540 }
541
542 const geom::LineString flat = geom::To2D(road.reference_line);
543 const double total = geom::Length(flat);
544 if (start_on_junction) {
545 JunctionLeg leg;
546 leg.road_id = road.id;
547 leg.contact = ContactPoint::Start;
548 leg.point = flat.front();
549 leg.heading = geom::NormalizeAngle(geom::HeadingAt(flat, 0.0) + M_PI);
550 leg.node = first_node;
551 legs.push_back(leg);
552 junction_of_node.emplace(first_node, std::string());
553 }
554 if (end_on_junction) {
555 JunctionLeg leg;
556 leg.road_id = road.id;
557 leg.contact = ContactPoint::End;
558 leg.point = flat.back();
559 leg.heading = geom::HeadingAt(flat, total);
560 leg.node = last_node;
561 legs.push_back(leg);
562 junction_of_node.emplace(last_node, std::string());
563 }
564 ends_of_road[road.id] = PieceEnds{first_node, last_node};
565 model.roads.push_back(std::move(road));
566 }
567 }
568
569 // Chain roads that meet at a node no junction owns: a street mapped as
570 // several consecutive OSM ways becomes one road, which is what keeps the
571 // leg count at a junction down.
572 {
573 std::map<long long, std::vector<std::string>> at_node;
574 for (const auto &entry : ends_of_road) {
575 at_node[entry.second.first].push_back(entry.first);
576 if (entry.second.last != entry.second.first) {
577 at_node[entry.second.last].push_back(entry.first);
578 }
579 }
580 bool merged_any = true;
581 while (merged_any) {
582 merged_any = false;
583 for (const auto &entry : at_node) {
584 const long long node = entry.first;
585 if (junction_nodes.count(node) != 0u || entry.second.size() != 2u) {
586 continue;
587 }
588 Road *a = nullptr;
589 Road *b = nullptr;
590 for (Road &road : model.roads) {
591 if (road.junction_id.has_value()) continue;
592 if (road.id == entry.second[0]) a = &road;
593 if (road.id == entry.second[1]) b = &road;
594 }
595 if (a == nullptr || b == nullptr || a == b) {
596 continue;
597 }
598 // a street keeps its identity across an OSM way break even when the
599 // class is tagged differently either side; the lane layout is what
600 // has to line up for the merged road to keep one lane section
601 if (a->lanes.size() != b->lanes.size()) {
602 continue;
603 }
604 bool same_layout = true;
605 for (std::size_t k = 0u; k < a->lanes.size(); ++k) {
606 if (a->lanes[k].id != b->lanes[k].id || a->lanes[k].type != b->lanes[k].type ||
607 std::abs(a->lanes[k].width - b->lanes[k].width) > 0.01) {
608 same_layout = false;
609 break;
610 }
611 }
612 if (!same_layout) {
613 continue;
614 }
615 PieceEnds &ea = ends_of_road[a->id];
616 PieceEnds &eb = ends_of_road[b->id];
617 // orient both so a ends at the node and b starts from it
618 auto flip_legs = [&legs](const std::string &road_id) {
619 for (JunctionLeg &leg : legs) {
620 if (leg.road_id == road_id) {
621 leg.contact = (leg.contact == ContactPoint::Start) ? ContactPoint::End
623 }
624 }
625 };
626 if (ea.first == node) {
627 std::reverse(a->reference_line.begin(), a->reference_line.end());
628 std::swap(ea.first, ea.last);
629 flip_legs(a->id);
630 }
631 if (eb.last == node) {
632 std::reverse(b->reference_line.begin(), b->reference_line.end());
633 std::swap(eb.first, eb.last);
634 flip_legs(b->id);
635 }
636 if (ea.last != node || eb.first != node) {
637 continue;
638 }
639 for (std::size_t k = 1u; k < b->reference_line.size(); ++k) {
640 a->reference_line.push_back(b->reference_line[k]);
641 }
642 ea.last = eb.last;
643 for (auto &list : at_node) {
644 for (std::string &id : list.second) {
645 if (id == b->id) {
646 id = a->id;
647 }
648 }
649 }
650 const std::string dead = b->id;
651 // b's far end becomes a's end; its legs move with it
652 for (JunctionLeg &leg : legs) {
653 if (leg.road_id == dead) {
654 leg.road_id = a->id;
655 leg.contact = ContactPoint::End;
656 }
657 }
658 ends_of_road.erase(dead);
659 model.roads.erase(std::remove_if(model.roads.begin(), model.roads.end(),
660 [&dead](const Road &r) { return r.id == dead; }), model.roads.end());
661 merged_any = true;
662 break;
663 }
664 }
665 }
666
667 // 3. one junction per shared node, with its legs attached
668 // A road with both ends in one cluster is internal to that junction: the
669 // junction's own connectors span it, and leaving it in strands them on a
670 // stub. lanegraph.py calls this junction_internal_m.
671 {
672 const double internal_limit = 2.0 * options.cluster_m;
673 std::map<std::string, std::vector<std::size_t>> legs_of_road;
674 for (std::size_t i = 0u; i < legs.size(); ++i) {
675 legs_of_road[legs[i].road_id].push_back(i);
676 }
677 std::set<std::string> internal;
678 for (const auto &entry : legs_of_road) {
679 if (entry.second.size() < 2u) {
680 continue;
681 }
682 const long long root = clusters.Find(legs[entry.second.front()].node);
683 bool same = true;
684 for (std::size_t i : entry.second) {
685 same = same && (clusters.Find(legs[i].node) == root);
686 }
687 if (!same) {
688 continue;
689 }
690 const Road *road = model.FindRoad(entry.first);
691 if (road != nullptr && LineLength(road->reference_line) <= internal_limit) {
692 internal.insert(entry.first);
693 }
694 }
695 if (!internal.empty()) {
696 model.roads.erase(std::remove_if(model.roads.begin(), model.roads.end(),
697 [&internal](const Road &r) { return internal.count(r.id) != 0u; }),
698 model.roads.end());
699 std::vector<JunctionLeg> surviving;
700 for (JunctionLeg &leg : legs) {
701 if (internal.count(leg.road_id) == 0u) {
702 surviving.push_back(leg);
703 }
704 }
705 legs = std::move(surviving);
706 }
707 }
708
709 std::map<long long, std::vector<std::size_t>> legs_of_node;
710 for (std::size_t i = 0u; i < legs.size(); ++i) {
711 legs_of_node[clusters.Find(legs[i].node)].push_back(i);
712 }
713
714 std::size_t junction_index = 0u;
715 std::vector<Road> connecting_roads;
716 for (const auto &entry : legs_of_node) {
717 if (entry.second.size() < 3u) {
718 // two legs only: the roads meet end to end, link them directly
719 if (entry.second.size() == 2u) {
720 JunctionLeg &a = legs[entry.second[0]];
721 JunctionLeg &b = legs[entry.second[1]];
722 Road *road_a = nullptr;
723 Road *road_b = nullptr;
724 for (Road &road : model.roads) {
725 if (road.id == a.road_id) road_a = &road;
726 if (road.id == b.road_id) road_b = &road;
727 }
728 if (road_a != nullptr && road_b != nullptr) {
729 RoadLink link_a{LinkElement::Road, b.road_id, b.contact};
730 RoadLink link_b{LinkElement::Road, a.road_id, a.contact};
731 (a.contact == ContactPoint::Start ? road_a->predecessor : road_a->successor) = link_a;
732 (b.contact == ContactPoint::Start ? road_b->predecessor : road_b->successor) = link_b;
733 // Two roads meeting end to end keep their lane numbering when the
734 // contacts are opposite, and mirror it when they are the same.
735 const bool mirrored = (a.contact == b.contact);
736 for (Lane &lane : road_a->lanes) {
737 const int target = mirrored ? -lane.id : lane.id;
738 bool exists = false;
739 for (const Lane &other : road_b->lanes) {
740 exists = exists || (other.id == target);
741 }
742 if (!exists) {
743 continue;
744 }
745 (a.contact == ContactPoint::Start ? lane.predecessor_lane
746 : lane.successor_lane) = target;
747 }
748 for (Lane &lane : road_b->lanes) {
749 const int target = mirrored ? -lane.id : lane.id;
750 bool exists = false;
751 for (const Lane &other : road_a->lanes) {
752 exists = exists || (other.id == target);
753 }
754 if (!exists) {
755 continue;
756 }
757 (b.contact == ContactPoint::Start ? lane.predecessor_lane
758 : lane.successor_lane) = target;
759 }
760 }
761 }
762 continue;
763 }
764
766 junction.id = MakeId("junction_", junction_index++);
767 // keyed on both ends: a road touching the junction at its start and at
768 // its end offers two different movements to the same target
769 std::map<std::tuple<std::string, ContactPoint, std::string, ContactPoint>,
770 std::pair<std::string, double>> best_movement;
771 for (long long node : junction_nodes) {
772 if (clusters.Find(node) == entry.first) {
773 junction.osm_node_ids.push_back(node);
774 }
775 }
776
777 for (std::size_t i : entry.second) {
778 for (std::size_t j : entry.second) {
779 if (i == j) {
780 continue;
781 }
782 const JunctionLeg &in = legs[i];
783 const JunctionLeg &out = legs[j];
784 // a leg never connects back to itself through the junction
785 // both headings point away from the junction, so a U-turn back down
786 // the same leg reads as ~0 and a straight-through as ~pi
787 const double turn = std::abs(geom::NormalizeAngle(out.heading - in.heading));
788 if (turn < (M_PI - options.uturn_deg * M_PI / 180.0)) {
789 continue;
790 }
791 const Road *road_in = model.FindRoad(in.road_id);
792 const Road *road_out = model.FindRoad(out.road_id);
793 if (road_in == nullptr || road_out == nullptr) {
794 continue;
795 }
796 std::vector<int> arriving;
797 std::vector<int> leaving;
798 std::vector<int> ignored;
799 SplitLanesAtContact(*road_in, in.contact, arriving, ignored);
800 ignored.clear();
801 SplitLanesAtContact(*road_out, out.contact, ignored, leaving);
802 if (arriving.empty() || leaving.empty()) {
803 continue;
804 }
805
806 Road connecting;
807 connecting.id = MakeId("road_", road_index++);
808 connecting.junction_id = junction.id;
809 connecting.highway = road_in->highway;
810 connecting.reference_line = ConnectingGeometry(
811 in.point, geom::NormalizeAngle(in.heading + M_PI),
812 out.point, out.heading,
813 road_in->reference_line.empty() ? 0.0 : road_in->reference_line.front().z);
814
815 const std::size_t width = std::min(arriving.size(), leaving.size());
816 for (std::size_t k = 0u; k < width; ++k) {
817 Lane lane;
818 lane.id = -static_cast<int>(k) - 1;
819 lane.type = LaneType::Driving;
820 lane.width = road_in->lanes.empty() ? 3.5 : road_in->lanes.front().width;
822 lane.predecessor_lane = arriving[k];
823 lane.successor_lane = leaving[k];
824 connecting.lanes.push_back(lane);
825 }
826 connecting.predecessor = RoadLink{LinkElement::Road, in.road_id, in.contact};
827 connecting.successor = RoadLink{LinkElement::Road, out.road_id, out.contact};
828
829 // one connector per movement: a cluster can hold several legs of the
830 // same road, which would otherwise serve the same turn twice
831 const std::tuple<std::string, ContactPoint, std::string, ContactPoint> movement{
832 in.road_id, in.contact, out.road_id, out.contact};
833 const double length = LineLength(connecting.reference_line);
834 const auto seen = best_movement.find(movement);
835 if (seen != best_movement.end()) {
836 if (seen->second.second <= length) {
837 --road_index;
838 continue;
839 }
840 const std::string superseded = seen->second.first;
841 connecting_roads.erase(
842 std::remove_if(connecting_roads.begin(), connecting_roads.end(),
843 [&superseded](const Road &r) { return r.id == superseded; }),
844 connecting_roads.end());
845 junction.connections.erase(
846 std::remove_if(junction.connections.begin(), junction.connections.end(),
847 [&superseded](const Connection &c) {
848 return c.connecting_road == superseded;
849 }),
850 junction.connections.end());
851 }
852 best_movement[movement] = std::make_pair(connecting.id, length);
853
854 Connection connection;
855 connection.id = MakeId("connection_", junction.connections.size());
856 connection.incoming_road = in.road_id;
857 connection.connecting_road = connecting.id;
859 for (std::size_t k = 0u; k < width; ++k) {
860 connection.lane_links.push_back(LaneLink{arriving[k], -static_cast<int>(k) - 1});
861 }
862 junction.connections.push_back(std::move(connection));
863 connecting_roads.push_back(std::move(connecting));
864 }
865 }
866
867 for (std::size_t i : entry.second) {
868 for (Road &road : model.roads) {
869 if (road.id != legs[i].road_id) {
870 continue;
871 }
872 RoadLink link{LinkElement::Junction, junction.id, std::nullopt};
873 if (legs[i].contact == ContactPoint::Start) {
874 road.predecessor = link;
875 } else {
876 road.successor = link;
877 }
878 }
879 }
880 model.junctions.push_back(std::move(junction));
881 }
882
883 for (Road &road : connecting_roads) {
884 model.roads.push_back(std::move(road));
885 }
886
887 if (options.clip.has_value()) {
888 ClipToBox(model, *options.clip);
889 }
890
891 return model;
892 }
893
894} // namespace twinmodel
float y
float x
float z
std::string highway
Definition LaneGraph.cpp:24
std::map< long long, long long > _parent
Definition LaneGraph.cpp:78
long long way_id
Definition LaneGraph.cpp:23
long long node
Definition LaneGraph.cpp:43
std::vector< long long > nodes
Definition LaneGraph.cpp:26
long long first
Definition LaneGraph.cpp:34
Tags tags
Definition LaneGraph.cpp:25
geom::Point point
Definition LaneGraph.cpp:41
double heading
Definition LaneGraph.cpp:42
long long last
Definition LaneGraph.cpp:35
std::string road_id
Definition LaneGraph.cpp:39
ContactPoint contact
Definition LaneGraph.cpp:40
std::map< std::string, unsigned int > road
std::map< std::string, unsigned int > junction
WGS84 <-> local ENU metres, a transverse Mercator centred on the origin.
Definition Frame.h:15
double GetOriginLongitude() const
Definition Frame.h:28
double GetOriginLatitude() const
Definition Frame.h:27
void ToLocal(double lon, double lat, double &x, double &y) const
Definition Frame.cpp:34
LineString Substring(const LineString &line, double from, double to)
The piece of line between distances from and to, shapely's substring: endpoints are inserted so the c...
double Length(const LineString &line)
double HeadingAt(const LineString &line, double s)
Heading of line at distance s, in radians.
std::vector< Point3 > Line3
bg::model::linestring< Point > LineString
double NormalizeAngle(double angle)
Signed angle in (-pi, pi] that turns heading from into heading to.
double Hypot(const Point &a, const Point &b)
bg::model::d2::point_xy< double > Point
bg::model::box< Point > Box
Point To2D(const Point3 &p)
LaneProfile ResolveProfile(const std::string &highway, const Tags &tags)
Resolve the lane layout from the way's tags, falling back to the class defaults.
Definition Profiles.cpp:96
const std::string * FindTag(const Tags &tags, const std::string &key)
Definition Model.h:20
bool IsDrivable(const std::string &highway)
True when the way carries motor traffic and should become a Road.
Definition Profiles.cpp:70
std::map< std::string, std::string > Tags
Definition Model.h:18
ContactPoint
Definition Model.h:40
TwinModelData BuildLaneGraph(const OsmData &osm, const LocalFrame &frame, const std::string &name, const LaneGraphOptions &options)
OSM ways -> roads, lanes and junctions in model space.
std::vector< LaneLink > lane_links
Definition Model.h:112
ContactPoint contact_point
Definition Model.h:111
std::string connecting_road
Definition Model.h:110
std::string id
Definition Model.h:108
std::string incoming_road
Definition Model.h:109
std::string id
Definition Model.h:116
std::optional< geom::Box > clip
Clip the network to this box in model metres, as the Python pipeline clips to the requested bbox.
Definition LaneGraph.h:38
double junction_radius
Radius around a junction node that the incoming roads are trimmed back by, so the connecting roads ha...
Definition LaneGraph.h:25
double cluster_m
Two intersection nodes join one junction when a chain shorter than this links them and they are close...
Definition LaneGraph.h:30
double min_road_length
Ways shorter than this are merged into their neighbour rather than becoming a road of their own.
Definition LaneGraph.h:21
double uturn_deg
Turns sharper than this are U-turns and get no connection; the profile's JunctionRules....
Definition LaneGraph.h:34
The lane layout an OSM way resolves to, before it is cut at junctions.
Definition Profiles.h:27
One lane of a road, numbered outward from the reference line.
Definition Model.h:68
LaneType type
Definition Model.h:70
Direction direction
Definition Model.h:72
double width
Definition Model.h:71
std::optional< int > successor_lane
Definition Model.h:78
std::optional< int > predecessor_lane
Lane this one continues from / into on the linked road; OpenDRIVE's <lane><link>, which is what lets ...
Definition Model.h:77
std::vector< OsmWay > ways
Definition OsmIngest.h:52
std::map< long long, OsmNode > nodes
Definition OsmIngest.h:51
std::vector< Lane > lanes
Definition Model.h:91
std::optional< RoadLink > successor
Definition Model.h:97
geom::Line3 reference_line
Definition Model.h:90
std::optional< RoadLink > predecessor
Definition Model.h:96
std::string id
Definition Model.h:89
std::optional< std::string > junction_id
Definition Model.h:95
std::string highway
Definition Model.h:93
Everything the OpenDRIVE writer reads.
Definition Model.h:158
std::vector< Road > roads
Definition Model.h:162
const Road * FindRoad(const std::string &id) const
Definition Model.h:168
std::vector< Junction > junctions
Definition Model.h:163