CARLA
Loading...
Searching...
No Matches
test_geo_projection.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 "test.h"
8
12#include <carla/geom/Location.h>
13
14#include <random>
15
23
24namespace {
25
26 // WGS84 ellipsoid (semi-major axis, inverse flattening).
27 Ellipsoid Wgs84() {
28 return Ellipsoid(6378137.0, 298.257223563);
29 }
30
31 // A Transverse Mercator projection anchored at the origin. Keeping
32 // x_0/y_0 at zero keeps projected coordinates small so the float
33 // storage in Location does not dominate the round-trip error.
36 TransverseMercatorParams(0.0, 0.0, 0.9996, 0.0, 0.0, Wgs84()));
37 }
38
39} // namespace
40
41// These tests replace the GeoLocation::InverseTransform coverage added in
42// PR #9726: the GeoProjection engine subsumes the standalone inverse, so the
43// equivalent round-trip and sign-convention guarantees are pinned here
44// against GeoProjection instead.
45
46TEST(GeoProjection, tm_round_trip_zero) {
47 // location -> geo -> location of the origin returns the origin within
48 // the double-to-float rounding tolerance of the projection.
49 const GeoProjection projection = TransverseMercator();
50 const Location origin{0.0f, 0.0f, 0.0f};
51
52 const GeoLocation geo = projection.TransformToGeoLocation(origin);
53 const Location back = projection.GeoLocationToTransform(geo);
54
55 ASSERT_NEAR(back.x, 0.0f, 1e-3f);
56 ASSERT_NEAR(back.y, 0.0f, 1e-3f);
57 ASSERT_NEAR(back.z, 0.0f, 1e-3f);
58}
59
60TEST(GeoProjection, tm_round_trip_random) {
61 // For ten deterministic random Locations within ±50 km of the origin,
62 // location -> geo -> location must recover the input.
63 std::mt19937 rng(42u);
64 std::uniform_real_distribution<float> xy_dist(-50000.0f, 50000.0f);
65 std::uniform_real_distribution<float> z_dist(-20.0f, 3000.0f);
66
67 const GeoProjection projection = TransverseMercator();
68 constexpr float tolerance_m = 0.1f;
69
70 for (int i = 0; i < 10; ++i) {
71 const Location input{xy_dist(rng), xy_dist(rng), z_dist(rng)};
72 const GeoLocation geo = projection.TransformToGeoLocation(input);
73 const Location back = projection.GeoLocationToTransform(geo);
74
75 ASSERT_NEAR(back.x, input.x, tolerance_m) << "iteration " << i;
76 ASSERT_NEAR(back.y, input.y, tolerance_m) << "iteration " << i;
77 ASSERT_NEAR(back.z, input.z, tolerance_m) << "iteration " << i;
78 }
79}
80
81TEST(GeoProjection, tm_preserves_relative_offsets) {
82 // Two Locations separated by a known XY delta must round-trip through
83 // the projection with the same delta. This pins the sign convention so
84 // a future projection change cannot silently flip an axis.
85 const GeoProjection projection = TransverseMercator();
86 const Location a{100.0f, 50.0f, 0.0f};
87 const Location b{120.0f, 70.0f, 0.0f};
88
89 const Location back_a =
90 projection.GeoLocationToTransform(projection.TransformToGeoLocation(a));
91 const Location back_b =
92 projection.GeoLocationToTransform(projection.TransformToGeoLocation(b));
93
94 constexpr float tolerance_m = 1e-2f;
95 ASSERT_NEAR(back_b.x - back_a.x, b.x - a.x, tolerance_m);
96 ASSERT_NEAR(back_b.y - back_a.y, b.y - a.y, tolerance_m);
97}
98
99TEST(GeoProjection, tm_north_is_minus_y) {
100 // CARLA locations are left-handed: north is -y and east is +x, as in the
101 // OpenDRIVE inertial frame with y negated. Pins the absolute orientation,
102 // which the round trips above cannot see.
103 const GeoProjection projection = TransverseMercator();
104 const GeoLocation origin = projection.TransformToGeoLocation(Location{0.0f, 0.0f, 0.0f});
105 const GeoLocation north = projection.TransformToGeoLocation(Location{0.0f, -100.0f, 0.0f});
106 const GeoLocation east = projection.TransformToGeoLocation(Location{100.0f, 0.0f, 0.0f});
107 ASSERT_GT(north.latitude, origin.latitude);
108 ASSERT_NEAR(north.longitude, origin.longitude, 1e-9);
109 ASSERT_GT(east.longitude, origin.longitude);
110 ASSERT_NEAR(east.latitude, origin.latitude, 1e-9);
111}
112
113TEST(GeoProjection, tm_altitude_passes_through) {
114 // Altitude is carried through the projection unchanged regardless of
115 // horizontal position.
116 const GeoProjection projection = TransverseMercator();
117 for (float z : {0.0f, 50.0f, 500.0f, 1000.0f, 3000.0f}) {
118 const Location input{10.0f, 10.0f, z};
119 const GeoLocation geo = projection.TransformToGeoLocation(input);
120 const Location back = projection.GeoLocationToTransform(geo);
121 ASSERT_NEAR(back.z, z, 1e-3f);
122 }
123}
124
125TEST(GeoProjection, utm_offset_round_trip) {
126 // Regression test: the UTM inverse transform applies the parsed
127 // OpenDRIVE offset to the input location, so the forward transform must
128 // apply the inverse offset. Without that symmetry a location -> geo ->
129 // location round-trip drifts by the full offset.
130 const OffsetTransform offset(500000.0, 4000000.0, 100.0, 0.0);
131 const GeoProjection projection = GeoProjection::Make(
132 UniversalTransverseMercatorParams(32, true, Wgs84(), offset));
133
134 constexpr float tolerance_m = 0.5f;
135 for (const Location &input :
136 {Location{0.0f, 0.0f, 0.0f}, Location{120.0f, -80.0f, 35.0f},
137 Location{-200.0f, 250.0f, -15.0f}}) {
138 const GeoLocation geo = projection.TransformToGeoLocation(input);
139 const Location back = projection.GeoLocationToTransform(geo);
140 ASSERT_NEAR(back.x, input.x, tolerance_m);
141 ASSERT_NEAR(back.y, input.y, tolerance_m);
142 ASSERT_NEAR(back.z, input.z, tolerance_m);
143 }
144}
145
146TEST(OffsetTransform, inverse_is_exact) {
147 // ApplyInverseTransformation must invert ApplyTransformation exactly,
148 // including the heading rotation and the z translation.
149 const OffsetTransform offset(12.5, -7.25, 3.0, 0.4);
150 constexpr float tolerance_m = 1e-3f;
151 for (const Location &input :
152 {Location{0.0f, 0.0f, 0.0f}, Location{100.0f, 200.0f, 30.0f},
153 Location{-150.0f, 75.0f, -10.0f}}) {
154 const Location forward = offset.ApplyTransformation(input);
155 const Location back = offset.ApplyInverseTransformation(forward);
156 ASSERT_NEAR(back.x, input.x, tolerance_m);
157 ASSERT_NEAR(back.y, input.y, tolerance_m);
158 ASSERT_NEAR(back.z, input.z, tolerance_m);
159 }
160}
float z
Location ApplyTransformation(const Location &location) const
Location ApplyInverseTransformation(const Location &location) const
Inverse of ApplyTransformation: recovers the pre-offset location so that ApplyTransformation(ApplyInv...
Location GeoLocationToTransform(const GeoLocation &geolocation) const
Transform the given location to a geo location.
GeoLocation TransformToGeoLocation(const Location &location) const
Transform the given geo location to a location.
static GeoProjection Make(T &&args)
TEST(GeoProjection, tm_round_trip_zero)