SMACC2
Loading...
Searching...
No Matches
geo_utils.hpp
Go to the documentation of this file.
1// Copyright 2026 RobosoftAI Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/*****************************************************************************************************************
16 *
17 * Authors: Brett Aldrich
18 *
19 ******************************************************************************************************************/
20
21#pragma once
22
23#include <algorithm>
24#include <cmath>
25#include <cstdint>
26#include <limits>
27#include <vector>
28
29// Geodetic helpers for cl_px4_mr.
30//
31// MapProjection is a verbatim port of PX4's azimuthal equidistant projection
32// (PX4-Autopilot/src/lib/geo/geo.cpp, spherical earth R = 6371000 m) so that
33// lat/lon projected here land on exactly the same local NED coordinates the
34// FMU uses for its own local position frame (whose origin is
35// VehicleLocalPosition.ref_lat/ref_lon). Do not swap this for an ellipsoidal
36// projection: the two disagree by ~0.3-0.5 % in scale, i.e. tens of metres at
37// 10 km.
38
39namespace cl_px4_mr
40{
41
42// WGS84 geographic point, degrees / metres
44{
45 double lat = 0.0;
46 double lon = 0.0;
47 double alt = 0.0;
48};
49
50// Local NED point (metres, z negative = up). yaw in radians, NaN = free.
52{
53 float x = 0.0f;
54 float y = 0.0f;
55 float z = 0.0f;
56 float yaw = std::numeric_limits<float>::quiet_NaN();
57};
58
59constexpr double kEarthRadiusM = 6371000.0; // == PX4 CONSTANTS_RADIUS_OF_EARTH
60
61namespace geo_detail
62{
63inline double radians(double deg) { return deg * (M_PI / 180.0); }
64inline double degrees(double rad) { return rad * (180.0 / M_PI); }
65inline double constrain(double v, double lo, double hi) { return std::min(std::max(v, lo), hi); }
66} // namespace geo_detail
67
68// Azimuthal equidistant projection about a reference lat/lon (PX4 MapProjection).
70{
71public:
72 MapProjection() = default;
73
74 MapProjection(double lat0, double lon0, uint64_t timestamp = 0)
75 {
76 initReference(lat0, lon0, timestamp);
77 }
78
79 void initReference(double lat0, double lon0, uint64_t timestamp = 0)
80 {
81 refTimestamp_ = timestamp;
84 refSinLat_ = std::sin(refLat_);
85 refCosLat_ = std::cos(refLat_);
86 initialized_ = true;
87 }
88
89 bool isInitialized() const { return initialized_; }
90 uint64_t referenceTimestamp() const { return refTimestamp_; }
91 double referenceLat() const { return geo_detail::degrees(refLat_); }
92 double referenceLon() const { return geo_detail::degrees(refLon_); }
93
94 // lat/lon in degrees -> x north, y east (metres)
95 void project(double lat, double lon, float & x, float & y) const
96 {
97 const double latRad = geo_detail::radians(lat);
98 const double lonRad = geo_detail::radians(lon);
99
100 const double sinLat = std::sin(latRad);
101 const double cosLat = std::cos(latRad);
102 const double cosDLon = std::cos(lonRad - refLon_);
103
104 const double arg =
105 geo_detail::constrain(refSinLat_ * sinLat + refCosLat_ * cosLat * cosDLon, -1.0, 1.0);
106 const double c = std::acos(arg);
107
108 double k = 1.0;
109 if (std::fabs(c) > 0.0)
110 {
111 k = c / std::sin(c);
112 }
113
114 x =
115 static_cast<float>(k * (refCosLat_ * sinLat - refSinLat_ * cosLat * cosDLon) * kEarthRadiusM);
116 y = static_cast<float>(k * cosLat * std::sin(lonRad - refLon_) * kEarthRadiusM);
117 }
118
119 // x north, y east (metres) -> lat/lon in degrees
120 void reproject(float x, float y, double & lat, double & lon) const
121 {
122 const double xRad = static_cast<double>(x) / kEarthRadiusM;
123 const double yRad = static_cast<double>(y) / kEarthRadiusM;
124 const double c = std::sqrt(xRad * xRad + yRad * yRad);
125
126 if (std::fabs(c) > 0.0)
127 {
128 const double sinC = std::sin(c);
129 const double cosC = std::cos(c);
130
131 const double latRad = std::asin(cosC * refSinLat_ + (xRad * sinC * refCosLat_) / c);
132 const double lonRad =
133 refLon_ + std::atan2(yRad * sinC, c * refCosLat_ * cosC - xRad * refSinLat_ * sinC);
134
135 lat = geo_detail::degrees(latRad);
136 lon = geo_detail::degrees(lonRad);
137 }
138 else
139 {
142 }
143 }
144
145private:
146 uint64_t refTimestamp_ = 0;
147 double refLat_ = 0.0;
148 double refLon_ = 0.0;
149 double refSinLat_ = 0.0;
150 double refCosLat_ = 1.0;
151 bool initialized_ = false;
152};
153
154// Great-circle distance in metres (haversine, spherical earth; same model as
155// PX4 get_distance_to_next_waypoint).
156inline double haversineDistance(double lat1, double lon1, double lat2, double lon2)
157{
158 const double p1 = geo_detail::radians(lat1);
159 const double p2 = geo_detail::radians(lat2);
160 const double dLat = p2 - p1;
161 const double dLon = geo_detail::radians(lon2 - lon1);
162
163 const double a = std::sin(dLat / 2.0) * std::sin(dLat / 2.0) +
164 std::sin(dLon / 2.0) * std::sin(dLon / 2.0) * std::cos(p1) * std::cos(p2);
165 const double c = 2.0 * std::atan2(std::sqrt(a), std::sqrt(1.0 - a));
166 return kEarthRadiusM * c;
167}
168
169inline double haversineDistance(const GeoPoint & a, const GeoPoint & b)
170{
171 return haversineDistance(a.lat, a.lon, b.lat, b.lon);
172}
173
174// Initial bearing from point 1 to point 2, radians, NED convention
175// (0 = north, +pi/2 = east), wrapped to [-pi, pi].
176inline double initialBearing(double lat1, double lon1, double lat2, double lon2)
177{
178 const double p1 = geo_detail::radians(lat1);
179 const double p2 = geo_detail::radians(lat2);
180 const double dLon = geo_detail::radians(lon2 - lon1);
181
182 const double y = std::sin(dLon) * std::cos(p2);
183 const double x = std::cos(p1) * std::sin(p2) - std::sin(p1) * std::cos(p2) * std::cos(dLon);
184 return std::atan2(y, x);
185}
186
187// Euclidean 3D distance between two NED points
188inline float nedDistance(const NedPoint & a, const NedPoint & b)
189{
190 const float dx = b.x - a.x;
191 const float dy = b.y - a.y;
192 const float dz = b.z - a.z;
193 return std::sqrt(dx * dx + dy * dy + dz * dz);
194}
195
196inline float nedDistanceXY(const NedPoint & a, const NedPoint & b)
197{
198 const float dx = b.x - a.x;
199 const float dy = b.y - a.y;
200 return std::sqrt(dx * dx + dy * dy);
201}
202
203// cumulative[i] = arc length from vertex 0 to vertex i (double accumulation,
204// cast to float at the end so long paths do not drift)
205inline std::vector<float> cumulativeLengths(const std::vector<NedPoint> & path)
206{
207 std::vector<float> cum;
208 cum.reserve(path.size());
209 double acc = 0.0;
210 for (size_t i = 0; i < path.size(); ++i)
211 {
212 if (i > 0)
213 {
214 acc += static_cast<double>(nedDistance(path[i - 1], path[i]));
215 }
216 cum.push_back(static_cast<float>(acc));
217 }
218 return cum;
219}
220
221inline float polylineLength(const std::vector<NedPoint> & path)
222{
223 if (path.empty())
224 {
225 return 0.0f;
226 }
227 return cumulativeLengths(path).back();
228}
229
230// Point at arc length s along the polyline (linear interpolation, clamped to
231// the ends). `cum` must be cumulativeLengths(path). Returns the segment index
232// containing s through `segmentIndex` (index of the segment start vertex).
234 const std::vector<NedPoint> & path, const std::vector<float> & cum, float s,
235 size_t * segmentIndex = nullptr)
236{
237 if (path.empty())
238 {
239 return NedPoint{};
240 }
241 if (path.size() == 1 || s <= 0.0f)
242 {
243 if (segmentIndex) *segmentIndex = 0;
244 return path.front();
245 }
246 if (s >= cum.back())
247 {
248 if (segmentIndex) *segmentIndex = path.size() - 2;
249 return path.back();
250 }
251
252 // first vertex whose cumulative length exceeds s
253 auto it = std::upper_bound(cum.begin(), cum.end(), s);
254 size_t i1 = static_cast<size_t>(it - cum.begin());
255 size_t i0 = i1 - 1;
256 const float segLen = cum[i1] - cum[i0];
257 const float t = segLen > 0.0f ? (s - cum[i0]) / segLen : 0.0f;
258
259 if (segmentIndex) *segmentIndex = i0;
260
261 NedPoint p;
262 p.x = path[i0].x + t * (path[i1].x - path[i0].x);
263 p.y = path[i0].y + t * (path[i1].y - path[i0].y);
264 p.z = path[i0].z + t * (path[i1].z - path[i0].z);
265 p.yaw = path[i0].yaw; // yaw is per-vertex, not interpolated
266 return p;
267}
268
269// Insert intermediate vertices so that no segment is longer than `spacing`.
270// Original vertices are kept (yaw preserved); inserted vertices carry NaN yaw.
271inline std::vector<NedPoint> resamplePolyline(const std::vector<NedPoint> & path, float spacing)
272{
273 std::vector<NedPoint> out;
274 if (path.empty() || spacing <= 0.0f)
275 {
276 return path;
277 }
278 out.push_back(path.front());
279 for (size_t i = 1; i < path.size(); ++i)
280 {
281 const NedPoint & a = path[i - 1];
282 const NedPoint & b = path[i];
283 const float len = nedDistance(a, b);
284 const int n = std::max(1, static_cast<int>(std::ceil(len / spacing)));
285 for (int k = 1; k < n; ++k)
286 {
287 const float t = static_cast<float>(k) / static_cast<float>(n);
288 NedPoint p;
289 p.x = a.x + t * (b.x - a.x);
290 p.y = a.y + t * (b.y - a.y);
291 p.z = a.z + t * (b.z - a.z);
292 out.push_back(p);
293 }
294 out.push_back(b);
295 }
296 return out;
297}
298
299} // namespace cl_px4_mr
void initReference(double lat0, double lon0, uint64_t timestamp=0)
Definition geo_utils.hpp:79
MapProjection(double lat0, double lon0, uint64_t timestamp=0)
Definition geo_utils.hpp:74
double referenceLat() const
Definition geo_utils.hpp:91
bool isInitialized() const
Definition geo_utils.hpp:89
double referenceLon() const
Definition geo_utils.hpp:92
void reproject(float x, float y, double &lat, double &lon) const
uint64_t referenceTimestamp() const
Definition geo_utils.hpp:90
void project(double lat, double lon, float &x, float &y) const
Definition geo_utils.hpp:95
double constrain(double v, double lo, double hi)
Definition geo_utils.hpp:65
double radians(double deg)
Definition geo_utils.hpp:63
double degrees(double rad)
Definition geo_utils.hpp:64
constexpr double kEarthRadiusM
Definition geo_utils.hpp:59
NedPoint sampleAtArcLength(const std::vector< NedPoint > &path, const std::vector< float > &cum, float s, size_t *segmentIndex=nullptr)
float nedDistance(const NedPoint &a, const NedPoint &b)
float polylineLength(const std::vector< NedPoint > &path)
float nedDistanceXY(const NedPoint &a, const NedPoint &b)
std::vector< NedPoint > resamplePolyline(const std::vector< NedPoint > &path, float spacing)
double initialBearing(double lat1, double lon1, double lat2, double lon2)
std::vector< float > cumulativeLengths(const std::vector< NedPoint > &path)
double haversineDistance(double lat1, double lon1, double lat2, double lon2)