diff options
| author | Rutger Broekhoff | 2026-09-08 13:54:40 +0200 |
|---|---|---|
| committer | Rutger Broekhoff | 2026-09-08 13:54:40 +0200 |
| commit | 1417e6cf41d27c594d2bdc92f87746d2383a1b1d (patch) | |
| tree | b7c5b811ed9da30fb74ea620407cf455e694c8a0 /server/src/geo/utm_multizonal.cppm | |
| parent | 5f9eee3f8562836e6a76b0b03a7b9a3592a4fbee (diff) | |
| download | routemon-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.cppm | 343 |
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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry.hpp> | ||
| 4 | #include <boost/geometry/srs/epsg.hpp> | ||
| 5 | #include <boost/geometry/srs/transformation.hpp> | ||
| 6 | |||
| 7 | export module routemon:geo.utm.multizonal; | ||
| 8 | |||
| 9 | import std; | ||
| 10 | import :geo; | ||
| 11 | import :geo.utm; | ||
| 12 | import :geo.utm.zone_local; | ||
| 13 | import :geo.wgs84; | ||
| 14 | |||
| 15 | namespace routemon::geo::utm::multizonal { | ||
| 16 | |||
| 17 | template <std::default_initializable T> | ||
| 18 | class multi_zone | ||
| 19 | { | ||
| 20 | std::array<T, zone::max().as_index() + 1> zones_; | ||
| 21 | |||
| 22 | public: | ||
| 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) | ||
| 29 | using from_wgs84_transform_base = | ||
| 30 | bgeo::srs::transformation<bgeo::srs::static_epsg<4326>>; | ||
| 31 | |||
| 32 | class from_wgs84_transform : public from_wgs84_transform_base | ||
| 33 | { | ||
| 34 | zone to_zone_; | ||
| 35 | |||
| 36 | public: | ||
| 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 | |||
| 53 | class 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 | |||
| 63 | public: | ||
| 64 | static from_wgs84_transforms const& instance() | ||
| 65 | { | ||
| 66 | static from_wgs84_transforms inst; | ||
| 67 | return inst; | ||
| 68 | } | ||
| 69 | }; | ||
| 70 | |||
| 71 | auto 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. | ||
| 109 | auto 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). | ||
| 175 | auto 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 | |||
| 205 | template <class T> | ||
| 206 | class linestring_rtree | ||
| 207 | { | ||
| 208 | public: | ||
| 209 | using index_value = std:: | ||
| 210 | tuple<utm::zone_local::prim::box, utm::zone_local::prim::linestring, T>; | ||
| 211 | |||
| 212 | private: | ||
| 213 | multi_zone<bgeo::index::rtree<index_value, bgeo::index::quadratic<16>>> | ||
| 214 | local_rtrees_; | ||
| 215 | |||
| 216 | public: | ||
| 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 | |||
| 270 | template <class T> | ||
| 271 | class point_rtree | ||
| 272 | { | ||
| 273 | public: | ||
| 274 | using index_value = | ||
| 275 | std::tuple<utm::zone_local::prim::box, utm::zone_local::prim::point, T>; | ||
| 276 | |||
| 277 | private: | ||
| 278 | multi_zone<bgeo::index::rtree<index_value, bgeo::index::quadratic<16>>> | ||
| 279 | local_rtrees_; | ||
| 280 | |||
| 281 | public: | ||
| 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 | ||