summaryrefslogtreecommitdiffstats
path: root/server/src/geo/utm_multizonal.cppm
diff options
context:
space:
mode:
authorRutger Broekhoff2026-09-08 13:54:40 +0200
committerRutger Broekhoff2026-09-08 13:54:40 +0200
commit1417e6cf41d27c594d2bdc92f87746d2383a1b1d (patch)
treeb7c5b811ed9da30fb74ea620407cf455e694c8a0 /server/src/geo/utm_multizonal.cppm
parent5f9eee3f8562836e6a76b0b03a7b9a3592a4fbee (diff)
downloadroutemon-1417e6cf41d27c594d2bdc92f87746d2383a1b1d.tar.gz
routemon-1417e6cf41d27c594d2bdc92f87746d2383a1b1d.zip
Some cleanups
Diffstat (limited to 'server/src/geo/utm_multizonal.cppm')
-rw-r--r--server/src/geo/utm_multizonal.cppm343
1 files changed, 343 insertions, 0 deletions
diff --git a/server/src/geo/utm_multizonal.cppm b/server/src/geo/utm_multizonal.cppm
new file mode 100644
index 0000000..540e309
--- /dev/null
+++ b/server/src/geo/utm_multizonal.cppm
@@ -0,0 +1,343 @@
1module;
2
3#include <boost/geometry.hpp>
4#include <boost/geometry/srs/epsg.hpp>
5#include <boost/geometry/srs/transformation.hpp>
6
7export module routemon:geo.utm.multizonal;
8
9import std;
10import :geo;
11import :geo.utm;
12import :geo.utm.zone_local;
13import :geo.wgs84;
14
15namespace routemon::geo::utm::multizonal {
16
17template <std::default_initializable T>
18class multi_zone
19{
20 std::array<T, zone::max().as_index() + 1> zones_;
21
22public:
23 auto operator[](zone z) -> T& { return zones_[z.as_index()]; }
24
25 auto operator[](zone z) const -> T const& { return zones_[z.as_index()]; }
26};
27
28// Transformations from WGS 84 (EPSG:4326)
29using from_wgs84_transform_base =
30 bgeo::srs::transformation<bgeo::srs::static_epsg<4326>>;
31
32class from_wgs84_transform : public from_wgs84_transform_base
33{
34 zone to_zone_;
35
36public:
37 explicit from_wgs84_transform(zone to_zone)
38 : from_wgs84_transform_base{{}, bgeo::srs::epsg{to_zone.wgs84_proj_epsg()}},
39 to_zone_{to_zone}
40 {
41 }
42
43 from_wgs84_transform() : from_wgs84_transform{zone::min()} {}
44
45 auto apply(zonable_wgs84_point p) -> zone_local::point
46 {
47 auto local_p = zone_local::point{to_zone_, 0.0, 0.0};
48 forward(p, local_p);
49 return local_p;
50 }
51};
52
53class from_wgs84_transforms : public multi_zone<from_wgs84_transform>
54{
55 from_wgs84_transforms()
56 {
57 for (auto z = utm::zone::min(); z != utm::zone::max(); z = z.next())
58 {
59 (*this)[z] = from_wgs84_transform{z};
60 }
61 }
62
63public:
64 static from_wgs84_transforms const& instance()
65 {
66 static from_wgs84_transforms inst;
67 return inst;
68 }
69};
70
71auto neighbor_utm_zone(utm::zonable_wgs84_point p)
72 -> std::pair<utm::zone, double>
73{
74 auto separating_meridian_lon = std::round(p.lon() / 6.0) * 6.0;
75 auto closest_zone_middle = separating_meridian_lon < p.lon()
76 ? separating_meridian_lon - 3.0
77 : separating_meridian_lon + 3.0;
78 auto closest_zone = utm::zone::for_wgs84_point(
79 utm::zonable_wgs84_point::from(
80 geo::wgs84::normalized_point{geo::wgs84::from_lat_lon(
81 p.lat(), closest_zone_middle)})
82 .value());
83
84 auto separating_meridian_ls = geo::wgs84::linestring{
85 geo::wgs84::from_lat_lon(90.0, 0.0),
86 geo::wgs84::from_lat_lon(0.0, separating_meridian_lon),
87 geo::wgs84::from_lat_lon(-90.0, 0.0),
88 };
89 auto closest_zone_dist = bgeo::distance(
90 p, separating_meridian_ls, geo::wgs84::vincenty_strategy{});
91
92 return std::make_pair(closest_zone, closest_zone_dist);
93}
94
95// Distribute a WGS 84 linestring over the UTM zones that it finds
96// itself in or near to.
97//
98// Since it is not at all unthinkable that some road works will end up
99// crossing different UTM zones, we need some machinery to work with
100// such situations. Since we want to report all situations within a
101// specified distance of the planned route, we must also consider the
102// situation where this is indeed the case, but the situation is
103// situated at the opposite side of a delineating UTM zone meridian
104// w.r.t. the planned route.
105//
106// To solve this issue, we create segments of the input line string
107// for multiple UTM zones when crossing zone boundaries or when very
108// close to zone boundaries.
109auto distribute_linestring_over_zones(utm::zonable_wgs84_linestring const& ls)
110 -> multi_zone<std::vector<utm::zone_local::prim::linestring>>
111{
112 auto result = multi_zone<std::vector<utm::zone_local::prim::linestring>>{};
113
114 auto to_utm = from_wgs84_transforms::instance();
115 auto working = multi_zone<utm::zone_local::prim::linestring>{};
116 auto mprev_p_zone = std::optional<utm::zone>{};
117 auto mprev_p_alt_zone = std::optional<utm::zone>{};
118
119 auto push = [&](utm::zonable_wgs84_point p, utm::zone z)
120 { working[z].push_back(to_utm[z].apply(p)); };
121 auto flush = [&](utm::zone z)
122 {
123 result[z].push_back(std::move(working[z]));
124 working[z] = {};
125 };
126
127 for (auto const& p : ls)
128 {
129 auto p_zone = utm::zone::for_wgs84_point(p);
130 auto mp_alt_zone = std::optional<utm::zone>{};
131 if (auto [neighbor_zone, neighbor_zone_dist] = neighbor_utm_zone(p);
132 neighbor_zone_dist < 30.0 /* m */)
133 mp_alt_zone = neighbor_zone;
134 assert(!mp_alt_zone || *mp_alt_zone != p_zone);
135
136 push(p, p_zone);
137 if (mp_alt_zone)
138 push(p, *mp_alt_zone);
139 if (mprev_p_zone && *mprev_p_zone != p_zone && mprev_p_zone != mp_alt_zone)
140 {
141 push(p, *mprev_p_zone);
142 flush(*mprev_p_zone);
143 }
144 if (mprev_p_alt_zone && *mprev_p_alt_zone != p_zone
145 && mprev_p_alt_zone != mp_alt_zone)
146 {
147 push(p, *mprev_p_alt_zone);
148 flush(*mprev_p_alt_zone);
149 }
150
151 mprev_p_zone = p_zone;
152 mprev_p_alt_zone = mp_alt_zone;
153 }
154
155 if (mprev_p_zone && !working[*mprev_p_zone].empty())
156 {
157 flush(*mprev_p_zone);
158 }
159 if (mprev_p_alt_zone && !working[*mprev_p_alt_zone].empty())
160 {
161 flush(*mprev_p_alt_zone);
162 }
163
164 return result;
165}
166
167// Split a WGS 84 line string into a sequence of UTM-zone-local line
168// strings, starting a new UTM-zone-local line string when the input
169// line string crosses a UTM zone boundary. The line segment that
170// crosses the zone boundary can be found in the UTM-zone-local line
171// strings for both zones which its points are in (we assume that line
172// segments will be short enough to not cause significant distortion
173// here, so we do not put in the effort to clip at the
174// zone-delineating meridian here).
175auto split_linestring_across_zones(utm::zonable_wgs84_linestring ls)
176 -> std::vector<utm::zone_local::linestring>
177{
178 auto splits = std::vector<utm::zone_local::linestring>{};
179
180 auto to_utm = from_wgs84_transforms::instance();
181 auto mworking_seg = std::optional<utm::zone_local::linestring>{};
182
183 for (auto const& p : ls)
184 {
185 auto p_zone = utm::zone::for_wgs84_point(p);
186 if (mworking_seg && mworking_seg->zone != p_zone)
187 {
188 mworking_seg->push_back(to_utm[mworking_seg->zone].apply(p));
189 splits.push_back(std::move(*mworking_seg));
190 mworking_seg = std::nullopt;
191 }
192 if (!mworking_seg)
193 mworking_seg = utm::zone_local::linestring{p_zone};
194 mworking_seg->push_back(to_utm[p_zone].apply(p));
195 }
196
197 if (mworking_seg && !mworking_seg->empty())
198 {
199 splits.push_back(std::move(*mworking_seg));
200 }
201
202 return splits;
203}
204
205template <class T>
206class linestring_rtree
207{
208public:
209 using index_value = std::
210 tuple<utm::zone_local::prim::box, utm::zone_local::prim::linestring, T>;
211
212private:
213 multi_zone<bgeo::index::rtree<index_value, bgeo::index::quadratic<16>>>
214 local_rtrees_;
215
216public:
217 template <std::convertible_to<T> U>
218 auto insert(utm::zonable_wgs84_linestring const& ls, U&& arg) -> void
219 {
220 auto zone_segments = distribute_linestring_over_zones(ls);
221 for (auto z = utm::zone::min(); z != utm::zone::max(); z = z.next())
222 {
223 for (auto ls : zone_segments[z])
224 {
225 auto blse = bgeo::return_buffer<utm::zone_local::prim::box>(
226 bgeo::return_envelope<utm::zone_local::prim::box>(ls),
227 30.0 /* m */);
228 local_rtrees_[z].insert(
229 index_value{blse, std::move(ls), std::forward<U>(arg)});
230 }
231 }
232 }
233
234 auto size() const -> std::size_t
235 {
236 auto total_size = 0uz;
237 for (auto z = utm::zone::min(); z != utm::zone::max(); z = z.next())
238 total_size += local_rtrees_[z].size();
239 return total_size;
240 }
241
242 // Note: the same T may be generated more than once!
243 auto intersection(std::vector<utm::zone_local::linestring> const& lss) const
244 -> std::generator<T const&>
245 {
246 for (auto const& ls : lss)
247 {
248 for (auto it = local_rtrees_[ls.zone].qbegin(bgeo::index::intersects(ls));
249 it != local_rtrees_[ls.zone].qend(); it++)
250 {
251 using multi_linestring =
252 bgeo::model::multi_linestring<utm::zone_local::prim::linestring>;
253
254 auto relevant_ls_parts = multi_linestring{};
255 bgeo::intersection(
256 std::get<0>(*it),
257 static_cast<utm::zone_local::prim::linestring const&>(ls),
258 relevant_ls_parts);
259 // it is somewhat awkward and arbitrary that this measurement is
260 // performed here
261 if (bgeo::distance(relevant_ls_parts, std::get<1>(*it)) < 30.0 /* m */)
262 {
263 co_yield std::get<2>(*it);
264 }
265 }
266 }
267 }
268};
269
270template <class T>
271class point_rtree
272{
273public:
274 using index_value =
275 std::tuple<utm::zone_local::prim::box, utm::zone_local::prim::point, T>;
276
277private:
278 multi_zone<bgeo::index::rtree<index_value, bgeo::index::quadratic<16>>>
279 local_rtrees_;
280
281public:
282 template <std::convertible_to<T> U>
283 auto insert(utm::zonable_wgs84_point const& p, U&& arg) -> void
284 {
285 auto to_utm = from_wgs84_transforms::instance();
286
287 auto p_zone = utm::zone::for_wgs84_point(p);
288 auto mp_alt_zone = std::optional<utm::zone>{};
289 if (auto [neighbor_zone, neighbor_zone_dist] = neighbor_utm_zone(p);
290 neighbor_zone_dist < 30.0 /* m */)
291 mp_alt_zone = neighbor_zone;
292
293 auto insert = [&](utm::zone z) -> void
294 {
295 auto p_utm = to_utm[z].apply(p);
296 auto bpe = bgeo::return_buffer<utm::zone_local::prim::box>(
297 bgeo::return_envelope<utm::zone_local::prim::box>(p_utm),
298 30.0 /* m */);
299 local_rtrees_[z].insert(index_value{bpe, p_utm, arg});
300 };
301
302 insert(p_zone);
303 if (mp_alt_zone)
304 insert(*mp_alt_zone);
305 }
306
307 auto size() const -> std::size_t
308 {
309 auto total_size = 0uz;
310 for (auto z = utm::zone::min(); z != utm::zone::max(); z = z.next())
311 total_size += local_rtrees_[z].size();
312 return total_size;
313 }
314
315 // Note: the same T may be generated more than once!
316 auto intersection(std::vector<utm::zone_local::linestring> const& lss) const
317 -> std::generator<T const&>
318 {
319 for (auto const& ls : lss)
320 {
321 for (auto it = local_rtrees_[ls.zone].qbegin(bgeo::index::intersects(ls));
322 it != local_rtrees_[ls.zone].qend(); it++)
323 {
324 using multi_linestring =
325 bgeo::model::multi_linestring<utm::zone_local::prim::linestring>;
326
327 auto relevant_ls_parts = multi_linestring{};
328 bgeo::intersection(
329 std::get<0>(*it),
330 static_cast<utm::zone_local::prim::linestring const&>(ls),
331 relevant_ls_parts);
332 // it is somewhat awkward and arbitrary that this measurement is
333 // performed here
334 if (bgeo::distance(relevant_ls_parts, std::get<1>(*it)) < 30.0 /* m */)
335 {
336 co_yield std::get<2>(*it);
337 }
338 }
339 }
340 }
341};
342
343} // namespace routemon::geo::utm::multizonal