CARLA
Loading...
Searching...
No Matches
osm2odr/Geometry.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 "Geometry.h"
8
9#include <algorithm>
10#include <map>
11#include <set>
12
13#include <boost/geometry/algorithms/correct.hpp>
14
15namespace twinmodel {
16namespace geom {
17
18 namespace {
19
20 // Vertices closer than this are the same node when the planar graph is
21 // built; OSM coordinates reach us already snapped to centimetres.
22 constexpr double kNodeEpsilon = 1e-6;
23
24 struct NodeKey {
25 long long x;
26 long long y;
27 bool operator<(const NodeKey &o) const {
28 return (x != o.x) ? (x < o.x) : (y < o.y);
29 }
30 };
31
32 NodeKey MakeKey(const Point &p) {
33 return NodeKey{
34 static_cast<long long>(std::llround(bg::get<0>(p) / kNodeEpsilon)),
35 static_cast<long long>(std::llround(bg::get<1>(p) / kNodeEpsilon))};
36 }
37
38 } // namespace
39
40 double Project(const LineString &line, const Point &p) {
41 if (line.size() < 2u) {
42 return 0.0;
43 }
44 double best_distance = std::numeric_limits<double>::max();
45 double best_s = 0.0;
46 double travelled = 0.0;
47 for (std::size_t i = 1u; i < line.size(); ++i) {
48 const Point &a = line[i - 1u];
49 const Point &b = line[i];
50 const double dx = bg::get<0>(b) - bg::get<0>(a);
51 const double dy = bg::get<1>(b) - bg::get<1>(a);
52 const double segment = std::hypot(dx, dy);
53 if (segment > 0.0) {
54 double t = ((bg::get<0>(p) - bg::get<0>(a)) * dx +
55 (bg::get<1>(p) - bg::get<1>(a)) * dy) / (segment * segment);
56 t = std::clamp(t, 0.0, 1.0);
57 const Point on(bg::get<0>(a) + t * dx, bg::get<1>(a) + t * dy);
58 const double distance = Hypot(on, p);
59 if (distance < best_distance) {
60 best_distance = distance;
61 best_s = travelled + t * segment;
62 }
63 }
64 travelled += segment;
65 }
66 return best_s;
67 }
68
69 Point Interpolate(const LineString &line, double s) {
70 if (line.empty()) {
71 return Point(0.0, 0.0);
72 }
73 if (s <= 0.0) {
74 return line.front();
75 }
76 double travelled = 0.0;
77 for (std::size_t i = 1u; i < line.size(); ++i) {
78 const double segment = Hypot(line[i - 1u], line[i]);
79 if (travelled + segment >= s) {
80 const double t = (segment > 0.0) ? (s - travelled) / segment : 0.0;
81 return Point(
82 bg::get<0>(line[i - 1u]) + t * (bg::get<0>(line[i]) - bg::get<0>(line[i - 1u])),
83 bg::get<1>(line[i - 1u]) + t * (bg::get<1>(line[i]) - bg::get<1>(line[i - 1u])));
84 }
85 travelled += segment;
86 }
87 return line.back();
88 }
89
90 double HeadingAt(const LineString &line, double s) {
91 if (line.size() < 2u) {
92 return 0.0;
93 }
94 double travelled = 0.0;
95 for (std::size_t i = 1u; i < line.size(); ++i) {
96 const double segment = Hypot(line[i - 1u], line[i]);
97 if (travelled + segment >= s || i + 1u == line.size()) {
98 return std::atan2(bg::get<1>(line[i]) - bg::get<1>(line[i - 1u]),
99 bg::get<0>(line[i]) - bg::get<0>(line[i - 1u]));
100 }
101 travelled += segment;
102 }
103 return 0.0;
104 }
105
106 LineString Substring(const LineString &line, double from, double to) {
107 LineString out;
108 if (line.size() < 2u) {
109 return out;
110 }
111 const double total = Length(line);
112 from = std::clamp(from, 0.0, total);
113 to = std::clamp(to, 0.0, total);
114 if (to <= from) {
115 return out;
116 }
117 bg::append(out, Interpolate(line, from));
118 double travelled = 0.0;
119 for (std::size_t i = 1u; i < line.size(); ++i) {
120 const double segment = Hypot(line[i - 1u], line[i]);
121 const double at_end = travelled + segment;
122 if (at_end > from && at_end < to) {
123 bg::append(out, line[i]);
124 }
125 travelled = at_end;
126 }
127 bg::append(out, Interpolate(line, to));
128 return out;
129 }
130
131 LineString OffsetLine(const LineString &line, double offset) {
132 LineString out;
133 if (line.size() < 2u) {
134 return out;
135 }
136 if (std::abs(offset) < 1e-12) {
137 return line;
138 }
139 // Per-vertex normal from the mean of the adjacent segment normals, scaled
140 // by 1/cos(half turn) so the offset keeps its width through a corner.
141 for (std::size_t i = 0u; i < line.size(); ++i) {
142 const std::size_t prev = (i == 0u) ? 0u : i - 1u;
143 const std::size_t next = (i + 1u == line.size()) ? i : i + 1u;
144 const double hx_in = bg::get<0>(line[i]) - bg::get<0>(line[prev]);
145 const double hy_in = bg::get<1>(line[i]) - bg::get<1>(line[prev]);
146 const double hx_out = bg::get<0>(line[next]) - bg::get<0>(line[i]);
147 const double hy_out = bg::get<1>(line[next]) - bg::get<1>(line[i]);
148 const double a_in = std::atan2(hy_in, hx_in);
149 const double a_out = std::atan2(hy_out, hx_out);
150 const double heading = (i == 0u) ? a_out
151 : (i + 1u == line.size()) ? a_in
152 : a_in + 0.5 * NormalizeAngle(a_out - a_in);
153 const double half_turn = (i == 0u || i + 1u == line.size())
154 ? 0.0 : 0.5 * NormalizeAngle(a_out - a_in);
155 const double scale = std::max(0.25, std::cos(half_turn));
156 bg::append(out, Point(
157 bg::get<0>(line[i]) - std::sin(heading) * offset / scale,
158 bg::get<1>(line[i]) + std::cos(heading) * offset / scale));
159 }
160 return out;
161 }
162
163 MultiPolygon UnaryUnion(const std::vector<Polygon> &polygons) {
164 MultiPolygon out;
165 for (const Polygon &polygon : polygons) {
166 Polygon corrected = polygon;
167 bg::correct(corrected);
168 if (!bg::is_valid(corrected)) {
169 corrected = MakeValid(corrected);
170 }
171 MultiPolygon merged;
172 bg::union_(out, corrected, merged);
173 out = std::move(merged);
174 }
175 return out;
176 }
177
178 Polygon MakeValid(const Polygon &polygon) {
179 Polygon out = polygon;
180 bg::correct(out);
181 if (bg::is_valid(out)) {
182 return out;
183 }
184 // A zero-width buffer is the standard GEOS repair and is what shapely's
185 // make_valid falls back to for self-touching rings.
186 MultiPolygon buffered;
187 bg::buffer(out, buffered,
188 bg::strategy::buffer::distance_symmetric<double>(0.0),
189 bg::strategy::buffer::side_straight(),
190 bg::strategy::buffer::join_miter(),
191 bg::strategy::buffer::end_flat(),
192 bg::strategy::buffer::point_square());
193 if (!buffered.empty()) {
194 std::size_t best = 0u;
195 double best_area = -1.0;
196 for (std::size_t i = 0u; i < buffered.size(); ++i) {
197 const double area = bg::area(buffered[i]);
198 if (area > best_area) {
199 best_area = area;
200 best = i;
201 }
202 }
203 return buffered[best];
204 }
205 return out;
206 }
207
208 std::vector<Polygon> Polygonize(const std::vector<LineString> &edges) {
209 // Planar face tracing: at each node the half-edges are sorted by bearing,
210 // and a face is walked by always taking the next half-edge clockwise.
211 struct HalfEdge {
212 std::size_t from = 0u;
213 std::size_t to = 0u;
214 double bearing = 0.0;
215 bool used = false;
216 };
217
218 std::map<NodeKey, std::size_t> node_ids;
219 std::vector<Point> nodes;
220 auto node_of = [&](const Point &p) {
221 const NodeKey key = MakeKey(p);
222 auto it = node_ids.find(key);
223 if (it != node_ids.end()) {
224 return it->second;
225 }
226 const std::size_t id = nodes.size();
227 nodes.push_back(p);
228 node_ids.emplace(key, id);
229 return id;
230 };
231
232 std::vector<HalfEdge> half_edges;
233 for (const LineString &edge : edges) {
234 for (std::size_t i = 1u; i < edge.size(); ++i) {
235 const std::size_t a = node_of(edge[i - 1u]);
236 const std::size_t b = node_of(edge[i]);
237 if (a == b) {
238 continue;
239 }
240 const double bearing = std::atan2(bg::get<1>(nodes[b]) - bg::get<1>(nodes[a]),
241 bg::get<0>(nodes[b]) - bg::get<0>(nodes[a]));
242 half_edges.push_back(HalfEdge{a, b, bearing, false});
243 half_edges.push_back(HalfEdge{b, a, NormalizeAngle(bearing + M_PI), false});
244 }
245 }
246
247 std::vector<std::vector<std::size_t>> outgoing(nodes.size());
248 for (std::size_t i = 0u; i < half_edges.size(); ++i) {
249 outgoing[half_edges[i].from].push_back(i);
250 }
251 for (auto &list : outgoing) {
252 std::sort(list.begin(), list.end(), [&](std::size_t a, std::size_t b) {
253 return half_edges[a].bearing < half_edges[b].bearing;
254 });
255 }
256
257 std::vector<Polygon> faces;
258 for (std::size_t start = 0u; start < half_edges.size(); ++start) {
259 if (half_edges[start].used) {
260 continue;
261 }
262 Ring ring;
263 std::size_t current = start;
264 bool closed = false;
265 for (std::size_t guard = 0u; guard < half_edges.size() + 1u; ++guard) {
266 half_edges[current].used = true;
267 bg::append(ring, nodes[half_edges[current].from]);
268 // the reverse half-edge, then one step clockwise around its target
269 const std::size_t node = half_edges[current].to;
270 const double back = NormalizeAngle(half_edges[current].bearing + M_PI);
271 const auto &list = outgoing[node];
272 if (list.empty()) {
273 break;
274 }
275 std::size_t pick = list.front();
276 double best = 10.0;
277 for (std::size_t candidate : list) {
278 double turn = NormalizeAngle(half_edges[candidate].bearing - back);
279 if (turn <= 0.0) {
280 turn += 2.0 * M_PI;
281 }
282 if (turn < best) {
283 best = turn;
284 pick = candidate;
285 }
286 }
287 current = pick;
288 if (current == start) {
289 closed = true;
290 break;
291 }
292 }
293 if (!closed || ring.size() < 3u) {
294 continue;
295 }
296 bg::append(ring, ring.front());
297 Polygon face;
298 face.outer() = ring;
299 bg::correct(face);
300 // the outer face of the arrangement comes out with the whole arrangement
301 // inside it, and is dropped by its negative signed area before correction
302 if (bg::area(face) > 0.0 && bg::is_valid(face)) {
303 faces.push_back(face);
304 }
305 }
306 return faces;
307 }
308
309} // namespace geom
310} // namespace twinmodel
float distance
float y
float x
long long node
Definition LaneGraph.cpp:43
std::vector< long long > nodes
Definition LaneGraph.cpp:26
double heading
Definition LaneGraph.cpp:42
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)
LineString OffsetLine(const LineString &line, double offset)
Offset line sideways by offset metres, positive to the left.
bg::model::ring< Point, true, true > Ring
Point Interpolate(const LineString &line, double s)
Point at distance s along line, shapely's LineString.interpolate.
std::vector< Polygon > Polygonize(const std::vector< LineString > &edges)
Build polygons from a set of noded edges, shapely's polygonize.
double HeadingAt(const LineString &line, double s)
Heading of line at distance s, in radians.
bg::model::linestring< Point > LineString
MultiPolygon UnaryUnion(const std::vector< Polygon > &polygons)
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::multi_polygon< Polygon > MultiPolygon
bg::model::d2::point_xy< double > Point
bg::model::polygon< Point, true, true > Polygon
double Project(const LineString &line, const Point &p)
Distance along line of the projection of p, shapely's LineString.project.
Polygon MakeValid(const Polygon &polygon)
bg::is_valid plus a correction pass, shapely's make_valid.