summaryrefslogtreecommitdiffstats
path: root/server/src/geo
diff options
context:
space:
mode:
authorRutger Broekhoff2026-09-08 01:36:38 +0200
committerRutger Broekhoff2026-09-08 01:37:34 +0200
commit3e8ce840c70ee00925210b96738696d1dc445f8f (patch)
tree4eb6b30b1d571daa596394c36e7769f6ee0af956 /server/src/geo
parent15bcf23f2e75f9c710eee3753d530ebefc5f78f1 (diff)
downloadroutemon-3e8ce840c70ee00925210b96738696d1dc445f8f.tar.gz
routemon-3e8ce840c70ee00925210b96738696d1dc445f8f.zip
UTM projection
Diffstat (limited to 'server/src/geo')
-rw-r--r--server/src/geo/geo.cppm7
-rw-r--r--server/src/geo/multizonal.cppm240
-rw-r--r--server/src/geo/utm.cppm199
-rw-r--r--server/src/geo/utm_zone_local.cppm74
-rw-r--r--server/src/geo/wgs84.cppm115
5 files changed, 635 insertions, 0 deletions
diff --git a/server/src/geo/geo.cppm b/server/src/geo/geo.cppm
new file mode 100644
index 0000000..446f1eb
--- /dev/null
+++ b/server/src/geo/geo.cppm
@@ -0,0 +1,7 @@
1module;
2
3#include <boost/geometry.hpp>
4
5export module routemon:geo;
6
7namespace bgeo = boost::geometry;
diff --git a/server/src/geo/multizonal.cppm b/server/src/geo/multizonal.cppm
new file mode 100644
index 0000000..f732994
--- /dev/null
+++ b/server/src/geo/multizonal.cppm
@@ -0,0 +1,240 @@
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.multizonal;
8
9import std;
10import :geo;
11import :geo.utm;
12import :geo.utm.zone_local;
13import :geo.wgs84;
14
15namespace routemon::geo::multizonal {
16
17template<std::default_initializable T>
18class multi_zone
19{
20 std::array<T, utm::zone::max().as_index() + 1> zones_;
21
22public:
23 auto operator[](utm::zone z) -> T&
24 {
25 return zones_[z.as_index()];
26 }
27
28 auto operator[](utm::zone z) const -> T const&
29 {
30 return zones_[z.as_index()];
31 }
32};
33
34namespace wgs84
35{
36
37// Transformations from WGS 84 (EPSG:4326)
38using transform_from_t = bgeo::srs::transformation<bgeo::srs::static_epsg<4326>>;
39
40class utm_transform : public transform_from_t
41{
42 utm::zone to_zone_;
43
44public:
45 explicit utm_transform(utm::zone to_zone)
46 : transform_from_t{{}, bgeo::srs::epsg{to_zone.wgs84_proj_epsg()}},
47 to_zone_{to_zone}
48 {
49 }
50
51 utm_transform()
52 : utm_transform{utm::zone::min()}
53 {}
54
55 auto apply(utm::zonable_wgs84_point p) -> utm::zone_local::point
56 {
57 auto local_p = utm::zone_local::point{to_zone_, 0.0, 0.0};
58 forward(p, local_p);
59 return local_p;
60 }
61};
62
63class utm_transforms : public multi_zone<utm_transform>
64{
65 utm_transforms()
66 {
67 for (auto z = utm::zone::min(); z != utm::zone::max(); z = z.next())
68 {
69 (*this)[z] = utm_transform{z};
70 }
71 }
72
73public:
74 static utm_transforms const& instance()
75 {
76 static utm_transforms inst;
77 return inst;
78 }
79};
80
81auto neighbor_utm_zone(utm::zonable_wgs84_point p) -> std::pair<utm::zone, double>
82{
83 auto separating_meridian_lon = std::round(p.lon() / 6.0) * 6.0;
84 auto closest_zone_middle = separating_meridian_lon < p.lon()
85 ? separating_meridian_lon - 3.0
86 : separating_meridian_lon + 3.0;
87 auto closest_zone = utm::zone::for_wgs84_point(utm::zonable_wgs84_point::from(geo::wgs84::normalized_point{geo::wgs84::from_lat_lon(p.lat(), closest_zone_middle)}).value());
88
89 auto separating_meridian_ls = geo::wgs84::linestring{
90 geo::wgs84::from_lat_lon(90.0, 0.0),
91 geo::wgs84::from_lat_lon(0.0, separating_meridian_lon),
92 geo::wgs84::from_lat_lon(-90.0, 0.0),
93 };
94 auto closest_zone_dist = bgeo::distance(p, separating_meridian_ls, geo::wgs84::vincenty_strategy{});
95
96 return std::make_pair(closest_zone, closest_zone_dist);
97}
98
99} // namespace wgs84
100
101struct multizone_linestring
102{
103 multi_zone<std::vector<utm::zone_local::prim::linestring>> segments;
104
105 // TODO: consider taking an input range instead
106 explicit multizone_linestring(utm::zonable_wgs84_linestring const& ls)
107 {
108 auto to_utm = wgs84::utm_transforms::instance();
109 auto working = multi_zone<utm::zone_local::prim::linestring>{};
110 auto mprev_p_zone = std::optional<utm::zone>{};
111 auto mprev_p_alt_zone = std::optional<utm::zone>{};
112
113 auto push = [&](utm::zonable_wgs84_point p, utm::zone z)
114 { working[z].push_back(to_utm[z].apply(p)); };
115 auto flush = [&](utm::zone z)
116 {
117 segments[z].push_back(std::move(working[z]));
118 working[z] = {};
119 };
120
121 for (auto const& p : ls)
122 {
123 auto p_zone = utm::zone::for_wgs84_point(p);
124 auto mp_alt_zone = std::optional<utm::zone>{};
125 if (auto [neighbor_zone, neighbor_zone_dist] = wgs84::neighbor_utm_zone(p);
126 neighbor_zone_dist < 30.0 /* m */)
127 mp_alt_zone = neighbor_zone;
128 assert(!mp_alt_zone || *mp_alt_zone != p_zone);
129
130 push(p, p_zone);
131 if (mp_alt_zone)
132 push(p, *mp_alt_zone);
133 if (mprev_p_zone && *mprev_p_zone != p_zone && mprev_p_zone != mp_alt_zone)
134 {
135 push(p, *mprev_p_zone);
136 flush(*mprev_p_zone);
137 }
138 if (mprev_p_alt_zone && *mprev_p_alt_zone != p_zone && mprev_p_alt_zone != mp_alt_zone)
139 {
140 push(p, *mprev_p_alt_zone);
141 flush(*mprev_p_alt_zone);
142 }
143
144 mprev_p_zone = p_zone;
145 mprev_p_alt_zone = mp_alt_zone;
146 }
147
148 if (mprev_p_zone && !working[*mprev_p_zone].empty())
149 {
150 flush(*mprev_p_zone);
151 }
152 if (mprev_p_alt_zone && !working[*mprev_p_alt_zone].empty())
153 {
154 flush(*mprev_p_alt_zone);
155 }
156 }
157};
158
159struct zoned_linestring_seg_seq
160{
161 std::vector<utm::zone_local::linestring> segments;
162
163 explicit zoned_linestring_seg_seq(utm::zonable_wgs84_linestring ls)
164 {
165 auto to_utm = wgs84::utm_transforms::instance();
166 auto mworking_seg = std::optional<utm::zone_local::linestring>{};
167
168 for (auto const& p : ls)
169 {
170 auto p_zone = utm::zone::for_wgs84_point(p);
171 if (mworking_seg && mworking_seg->zone != p_zone)
172 {
173 mworking_seg->push_back(to_utm[mworking_seg->zone].apply(p));
174 segments.push_back(std::move(*mworking_seg));
175 mworking_seg = std::nullopt;
176 }
177 if (!mworking_seg)
178 mworking_seg = utm::zone_local::linestring{p_zone};
179 mworking_seg->push_back(to_utm[p_zone].apply(p));
180 }
181
182 if (mworking_seg && !mworking_seg->empty())
183 {
184 segments.push_back(std::move(*mworking_seg));
185 }
186 }
187};
188
189template<class T>
190class linestring_rtree
191{
192public:
193 using index_value = std::tuple<utm::zone_local::prim::box, utm::zone_local::prim::linestring, T>;
194
195private:
196 multi_zone<bgeo::index::rtree<index_value, bgeo::index::quadratic<16>>> local_rtrees_;
197
198public:
199 template<std::convertible_to<T> U>
200 auto insert(utm::zonable_wgs84_linestring const& ls, U&& arg) -> void
201 {
202 auto zone_segments = multizone_linestring{ls}.segments;
203 for (auto z = utm::zone::min(); z != utm::zone::max(); z = z.next())
204 {
205 for (auto ls : zone_segments[z])
206 {
207 utm::zone_local::prim::box blse =
208 bgeo::return_buffer<utm::zone_local::prim::box>(bgeo::return_envelope<utm::zone_local::prim::box>(ls), 30.0 /* m */);
209 local_rtrees_[z].insert(index_value{blse, std::move(ls), std::forward<U>(arg)});
210 }
211 }
212 }
213
214 auto size() const -> std::size_t
215 {
216 auto total_size = 0uz;
217 for (auto z = utm::zone::min(); z != utm::zone::max(); z = z.next())
218 total_size += local_rtrees_[z].size();
219 return total_size;
220 }
221
222 // Note: the same T may be generated more than once!
223 auto intersection(zoned_linestring_seg_seq const& lss) const -> std::generator<T const&>
224 {
225 for (auto const& ls : lss.segments)
226 {
227 // auto ls_box = bgeo::return_envelope<utm::zone_local::prim::box>(ls);
228 for (auto it = local_rtrees_[ls.zone].qbegin(bgeo::index::intersects(ls));
229 it != local_rtrees_[ls.zone].qend(); it++)
230 {
231 if (bgeo::intersects(ls, std::get<1>(*it)))
232 {
233 co_yield std::get<2>(*it);
234 }
235 }
236 }
237 }
238};
239
240} // namespace routemon::geo::multizonal
diff --git a/server/src/geo/utm.cppm b/server/src/geo/utm.cppm
new file mode 100644
index 0000000..83da222
--- /dev/null
+++ b/server/src/geo/utm.cppm
@@ -0,0 +1,199 @@
1module;
2
3#include <cassert>
4#include <boost/geometry/geometries/geometries.hpp>
5
6export module routemon:geo.utm;
7
8import std;
9import :geo;
10import :geo.wgs84;
11
12namespace routemon::geo::utm
13{
14
15class zonable_wgs84_point
16{
17 wgs84::normalized_point p_;
18
19 explicit zonable_wgs84_point(wgs84::normalized_point p)
20 : p_{p}
21 {}
22
23public:
24 zonable_wgs84_point()
25 : p_{}
26 {
27 }
28
29 static auto from(wgs84::normalized_point p) -> std::optional<zonable_wgs84_point>
30 {
31 if (p.lat() < -80 || p.lat() > 84)
32 return std::nullopt;
33 return zonable_wgs84_point{p};
34 }
35
36 auto lat() const -> double
37 {
38 return p_.lat();
39 }
40
41 auto lon() const -> double
42 {
43 return p_.lon();
44 }
45
46 auto lon(double lon) -> void
47 {
48 p_.lon(lon);
49 }
50
51 auto lat(double lat) -> void
52 {
53 if (lat < -80 || lat > 84)
54 throw std::range_error{"latitude out of range for UTM"};
55 p_.lat(lat);
56 }
57};
58
59}
60
61namespace boost::geometry::traits {
62
63 namespace {
64
65 using zonable_wgs84_point = routemon::geo::utm::zonable_wgs84_point;
66
67 } // namespace <anonymous>
68
69 template<> struct tag<zonable_wgs84_point> { using type = point_tag; };
70 template<> struct dimension<zonable_wgs84_point> : boost::mpl::int_<2> {};
71 template<> struct coordinate_type<zonable_wgs84_point> { using type = double; };
72 template<> struct coordinate_system<zonable_wgs84_point> { using type = routemon::geo::wgs84::cs; };
73
74 template<std::size_t index>
75 struct access<zonable_wgs84_point, index> {
76 static inline auto get(zonable_wgs84_point const& p) -> double
77 {
78 if constexpr (index == 0)
79 return p.lon();
80 else if constexpr (index == 1)
81 return p.lat();
82 else static_assert(false, "Out of range");
83 }
84
85 static inline auto set(zonable_wgs84_point& p, double v) -> void
86 {
87 if constexpr (index == 0)
88 p.lon(v);
89 else if constexpr (index == 1)
90 p.lat(v);
91 else static_assert(false, "Out of range");
92 }
93 };
94
95} // namespace boost::geometry::traits
96
97namespace routemon::geo::utm
98{
99
100using zonable_wgs84_linestring = bgeo::model::linestring<zonable_wgs84_point, std::vector>;
101
102// In the sense of the common WGS84 subdivisions by simple northing
103// and easting (so no Norway/Svalbard exceptions).
104class zone
105{
106 // Note: calculations heavily depend on the values of the variants.
107 enum class hemisphere : std::uint8_t
108 {
109 northern = 0,
110 southern = 1,
111 };
112
113 // Negative if in the southern hemisphere
114 // Equal to (zone_no - 1) * 2 + hemisphere
115 // In range [0, 119]
116 std::uint8_t zone_;
117
118 static constexpr auto zone_no_valid(std::uint8_t zone_no) -> bool
119 {
120 return 0 < zone_no && zone_no <= 60;
121 }
122
123 static constexpr auto from(std::uint8_t zone_no, hemisphere h) -> std::uint8_t
124 {
125 if (!zone_no_valid(zone_no))
126 throw std::invalid_argument{"invalid UTM zone number"};
127 return (zone_no - 1) * 2 + static_cast<std::uint8_t>(h);
128 }
129
130public:
131 explicit constexpr zone(std::uint8_t zone_no, hemisphere h)
132 : zone_{from(zone_no, h)}
133 {}
134
135 auto hemisphere() const -> enum hemisphere
136 {
137 return static_cast<enum hemisphere>(zone_ % 2);
138 }
139
140 // Return value in range [1, 60]
141 auto zone_no() const -> std::uint8_t
142 {
143 return 1 + zone_ / 2;
144 }
145
146 // Return value in range [min(), max()]
147 constexpr auto as_index() const -> std::uint8_t
148 {
149 return zone_;
150 }
151
152 static auto for_wgs84_point(zonable_wgs84_point p) noexcept -> zone
153 {
154 auto zone_no = static_cast<std::uint8_t>(1 + (p.lon() + 180.0) / 6.0);
155 auto northern = p.lat() >= 0.0;
156 return zone{zone_no, northern ? hemisphere::northern : hemisphere::southern};
157 }
158
159 auto wgs84_proj_epsg() const -> int
160 {
161 switch (hemisphere())
162 {
163 case hemisphere::northern:
164 return 32600 + zone_no();
165 case hemisphere::southern:
166 return 32700 + zone_no();
167 }
168 }
169
170 // Guarantee: min().as_index() == 0.
171 static constexpr auto min() noexcept -> zone
172 {
173 return zone{1, hemisphere::northern};
174 }
175
176 static constexpr auto max() noexcept -> zone
177 {
178 return zone{60, hemisphere::southern};
179 }
180
181 auto next() const -> zone
182 {
183 assert(zone_ <= max().as_index());
184 if (zone_ == max().as_index())
185 throw std::range_error{"cannot take next of greatest UTM zone"};
186 auto copy = zone{*this};
187 copy.zone_++;
188 return copy;
189 }
190
191 auto operator==(zone rhs) const -> bool
192 {
193 return zone_ == rhs.zone_;
194 }
195};
196static_assert(zone::min().as_index() == 0);
197static_assert(zone::max().as_index() == 119);
198
199} // namespace routemon::geo::utm
diff --git a/server/src/geo/utm_zone_local.cppm b/server/src/geo/utm_zone_local.cppm
new file mode 100644
index 0000000..98a1344
--- /dev/null
+++ b/server/src/geo/utm_zone_local.cppm
@@ -0,0 +1,74 @@
1module;
2
3#include <boost/geometry.hpp>
4#include <boost/geometry/geometries/register/linestring.hpp>
5
6export module routemon:geo.utm.zone_local;
7
8import :geo;
9import :geo.utm;
10
11// Coordinates that are local to some known UTM zone.
12namespace routemon::geo::utm::zone_local {
13
14 using cs = bgeo::cs::cartesian;
15
16 namespace prim {
17
18 using point = bgeo::model::d2::point_xy<double>;
19 using linestring = bgeo::model::linestring<point>;
20 using box = bgeo::model::box<point>;
21
22 } // namespace prim
23
24 struct point : public prim::point
25 {
26 zone zone;
27
28 explicit point(class zone zone, double x, double y)
29 : prim::point{x, y}, zone{zone}
30 {
31 }
32 };
33
34 struct linestring : public prim::linestring
35 {
36 zone zone;
37
38 explicit linestring(class zone zone)
39 : zone{zone}
40 {
41 }
42 };
43
44} // namespace routemon::geo::utm::zone_local
45
46BOOST_GEOMETRY_REGISTER_LINESTRING(routemon::geo::utm::zone_local::linestring)
47
48namespace boost::geometry::traits {
49
50 namespace {
51
52 using zone_local_point = routemon::geo::utm::zone_local::point;
53
54 } // namespace <anonymous>
55
56 template<> struct tag<zone_local_point> { using type = point_tag; };
57 template<> struct dimension<zone_local_point> : boost::mpl::int_<2> {};
58 template<> struct coordinate_type<zone_local_point> { using type = double; };
59 template<> struct coordinate_system<zone_local_point> { using type = routemon::geo::utm::zone_local::cs; };
60
61 template<std::size_t index>
62 struct access<zone_local_point, index> {
63 static inline auto get(zone_local_point const& p) -> double
64 {
65 return bgeo::get<index>(static_cast<routemon::geo::utm::zone_local::prim::point const&>(p));
66 }
67
68 static inline auto set(zone_local_point& p, double v) -> void
69 {
70 return bgeo::set<index>(static_cast<routemon::geo::utm::zone_local::prim::point&>(p), v);
71 }
72 };
73
74} // namespace boost::geometry::traits
diff --git a/server/src/geo/wgs84.cppm b/server/src/geo/wgs84.cppm
new file mode 100644
index 0000000..d574d39
--- /dev/null
+++ b/server/src/geo/wgs84.cppm
@@ -0,0 +1,115 @@
1module;
2
3#include <boost/geometry.hpp>
4
5export module routemon:geo.wgs84;
6
7import :geo;
8
9namespace routemon::geo::wgs84 {
10
11using cs = bgeo::cs::geographic<bgeo::degree>;
12using point = bgeo::model::point<double, 2, cs>;
13// TODO: guarantee that linestring provides non-static member
14// auto reserve(std::size_t) -> void
15// Perhaps better yet: guarantee that the backing container is a std::vector.
16using linestring = bgeo::model::linestring<point, std::vector /* the default */>;
17using box = bgeo::model::box<point>;
18using stype = bgeo::srs::spheroid<double>;
19using vincenty_strategy = bgeo::strategy::distance::vincenty<stype>;
20
21auto from_lat_lon(double lat, double lon) -> point
22{
23 return point{lon, lat};
24}
25
26auto lat(point p) -> double
27{
28 return bgeo::get<1>(p);
29}
30
31auto lon(point p) -> double
32{
33 return bgeo::get<0>(p);
34}
35
36// Point p with latitude in [-90, 90] and longitude in [-180, 180)
37auto normalize(point p) -> point
38{
39 // Equivalent degrees in the range [0, 360)
40 auto nonneg_mod360 = [](double t) -> double
41 { return std::remainder(std::remainder(t, 360.0) + 360.0, 360.0); };
42
43 auto p_lon = lon(p);
44 auto p_lat = nonneg_mod360(lat(p)) - 90.0;
45 if (90.0 < p_lat)
46 {
47 assert(p_lat < 270.0); // by nonneg_mod360
48 p_lon += 180.0;
49 p_lat -= 180.0;
50 }
51 p_lon = nonneg_mod360(p_lon + 180.0) - 180.0;
52
53 return from_lat_lon(p_lat, p_lon);
54}
55
56auto is_normalized(point p) -> bool
57{
58 return -90 <= lat(p) && lat(p) <= 90 && -180 <= lon(p) && lon(p) < 180;
59}
60
61class normalized_point
62{
63 point p_;
64
65public:
66 normalized_point(point p)
67 : p_{is_normalized(p) ? p : normalize(p)}
68 {}
69
70 normalized_point()
71 : p_{}
72 {}
73
74 auto lat() const -> double
75 {
76 return bgeo::get<1>(p_);
77 }
78
79 auto lon() const -> double
80 {
81 return bgeo::get<0>(p_);
82 }
83
84 auto lat(double lat) -> void
85 {
86 if (lat < -90 || lat > 90)
87 throw std::range_error{"latitude out of range"};
88 bgeo::set<1>(p_, lat);
89 }
90
91 auto lon(double lon) -> void
92 {
93 if (lon < -180 || lon > 180)
94 throw std::range_error{"longitude out of range"};
95 bgeo::set<0>(p_, lon);
96 }
97};
98
99class normalized_linestring
100{
101 std::vector<normalized_point> ls_;
102
103public:
104 auto empty() const -> bool
105 {
106 return ls_.empty();
107 }
108
109 auto push_back(normalized_point p) -> void
110 {
111 ls_.push_back(p);
112 }
113};
114
115} // namespace routemon::geo::wgs84