Coverage Report

Created: 2026-05-30 06:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/PROJ/src/conversions/cart.cpp
Line
Count
Source
1
/******************************************************************************
2
 * Project:  PROJ.4
3
 * Purpose:  Convert between ellipsoidal, geodetic coordinates and
4
 *           cartesian, geocentric coordinates.
5
 *
6
 *           Formally, this functionality is also found in the PJ_geocent.c
7
 *           code.
8
 *
9
 *           Actually, however, the PJ_geocent transformations are carried
10
 *           out in concert between 2D stubs in PJ_geocent.c and 3D code
11
 *           placed in pj_transform.c.
12
 *
13
 *           For pipeline-style datum shifts, we do need direct access
14
 *           to the full 3D interface for this functionality.
15
 *
16
 *           Hence this code, which may look like "just another PJ_geocent"
17
 *           but really is something substantially different.
18
 *
19
 * Author:   Thomas Knudsen, thokn@sdfe.dk
20
 *
21
 ******************************************************************************
22
 * Copyright (c) 2016, Thomas Knudsen / SDFE
23
 *
24
 * Permission is hereby granted, free of charge, to any person obtaining a
25
 * copy of this software and associated documentation files (the "Software"),
26
 * to deal in the Software without restriction, including without limitation
27
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
28
 * and/or sell copies of the Software, and to permit persons to whom the
29
 * Software is furnished to do so, subject to the following conditions:
30
 *
31
 * The above copyright notice and this permission notice shall be included
32
 * in all copies or substantial portions of the Software.
33
 *
34
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
35
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
37
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
39
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
40
 * DEALINGS IN THE SOFTWARE.
41
 *****************************************************************************/
42
43
#include "proj_internal.h"
44
#include <math.h>
45
46
PROJ_HEAD(cart, "Geodetic/cartesian conversions");
47
48
/**************************************************************
49
                CARTESIAN / GEODETIC CONVERSIONS
50
***************************************************************
51
    This material follows:
52
53
    Bernhard Hofmann-Wellenhof & Helmut Moritz:
54
    Physical Geodesy, 2nd edition.
55
    Springer, 2005.
56
57
    chapter 5.6: Coordinate transformations
58
    (HM, below),
59
60
    and
61
62
    Wikipedia: Geographic Coordinate Conversion,
63
    https://en.wikipedia.org/wiki/Geographic_coordinate_conversion
64
65
    (WP, below).
66
67
    The cartesian-to-geodetic conversion is based on Bowring's
68
    celebrated method:
69
70
    B. R. Bowring:
71
    Transformation from spatial to geographical coordinates
72
    Survey Review 23(181), pp. 323-327, 1976
73
74
    (BB, below),
75
76
    but could probably use some TLC from a newer and faster
77
    algorithm:
78
79
    Toshio Fukushima:
80
    Transformation from Cartesian to Geodetic Coordinates
81
    Accelerated by Halley’s Method
82
    Journal of Geodesy, February 2006
83
84
    (TF, below).
85
86
    Close to the poles, we avoid singularities by switching to an
87
    approximation requiring knowledge of the geocentric radius
88
    at the given latitude. For this, we use an adaptation of the
89
    formula given in:
90
91
    Wikipedia: Earth Radius
92
    https://en.wikipedia.org/wiki/Earth_radius#Radius_at_a_given_geodetic_latitude
93
    (Derivation and commentary at https://gis.stackexchange.com/q/20200)
94
95
    (WP2, below)
96
97
    These routines are probably not as robust at those in
98
    geocent.c, at least they haven't been through as heavy
99
    use as their geocent sisters. Some care has been taken
100
    to avoid singularities, but extreme cases (e.g. setting
101
    es, the squared eccentricity, to 1), will cause havoc.
102
103
**************************************************************/
104
105
/*********************************************************************/
106
1.04M
static double normal_radius_of_curvature(double a, double es, double sinphi) {
107
    /*********************************************************************/
108
1.04M
    if (es == 0)
109
26.7k
        return a;
110
    /* This is from WP.  HM formula 2-149 gives an a,b version */
111
1.02M
    return a / sqrt(1 - es * sinphi * sinphi);
112
1.04M
}
113
114
/*********************************************************************/
115
static double geocentric_radius(double a, double b_div_a, double cosphi,
116
81
                                double sinphi) {
117
    /*********************************************************************
118
        Return the geocentric radius at latitude phi, of an ellipsoid
119
        with semimajor axis a and semiminor axis b.
120
121
        This is from WP2, but uses hypot() for potentially better
122
        numerical robustness
123
    ***********************************************************************/
124
    // Non-optimized version:
125
    // const double b = a * b_div_a;
126
    // return hypot(a * a * cosphi, b * b * sinphi) /
127
    //        hypot(a * cosphi, b * sinphi);
128
81
    const double cosphi_squared = cosphi * cosphi;
129
81
    const double sinphi_squared = sinphi * sinphi;
130
81
    const double b_div_a_squared = b_div_a * b_div_a;
131
81
    const double b_div_a_squared_mul_sinphi_squared =
132
81
        b_div_a_squared * sinphi_squared;
133
81
    return a * sqrt((cosphi_squared +
134
81
                     b_div_a_squared * b_div_a_squared_mul_sinphi_squared) /
135
81
                    (cosphi_squared + b_div_a_squared_mul_sinphi_squared));
136
81
}
137
138
/*********************************************************************/
139
561k
static PJ_XYZ cartesian(PJ_LPZ geod, PJ *P) {
140
    /*********************************************************************/
141
561k
    PJ_XYZ xyz;
142
143
561k
    const double cosphi = cos(geod.phi);
144
561k
    const double sinphi = sin(geod.phi);
145
561k
    const double N = normal_radius_of_curvature(P->a, P->es, sinphi);
146
147
    /* HM formula 5-27 (z formula follows WP) */
148
561k
    xyz.x = (N + geod.z) * cosphi * cos(geod.lam);
149
561k
    xyz.y = (N + geod.z) * cosphi * sin(geod.lam);
150
561k
    xyz.z = (N * (1 - P->es) + geod.z) * sinphi;
151
152
561k
    return xyz;
153
561k
}
154
155
/*********************************************************************/
156
485k
static PJ_LPZ geodetic(PJ_XYZ cart, PJ *P) {
157
    /*********************************************************************/
158
485k
    PJ_LPZ lpz;
159
160
    // Normalize (x,y,z) to the unit sphere/ellipsoid.
161
#if (defined(__i386__) && !defined(__SSE__)) || defined(_M_IX86)
162
    // i386 (actually non-SSE) code path to make following test case of
163
    // testvarious happy
164
    // "echo 6378137.00 -0.00 0.00 | bin/cs2cs +proj=geocent +datum=WGS84 +to
165
    // +proj=latlong +datum=WGS84"
166
    const double x_div_a = cart.x / P->a;
167
    const double y_div_a = cart.y / P->a;
168
    const double z_div_a = cart.z / P->a;
169
#else
170
485k
    const double x_div_a = cart.x * P->ra;
171
485k
    const double y_div_a = cart.y * P->ra;
172
485k
    const double z_div_a = cart.z * P->ra;
173
485k
#endif
174
175
    /* Perpendicular distance from point to Z-axis (HM eq. 5-28) */
176
485k
    const double p_div_a = sqrt(x_div_a * x_div_a + y_div_a * y_div_a);
177
178
#if 0
179
    /* HM eq. (5-37) */
180
    const double theta  =  atan2 (cart.z * P->a,  p * P->b);
181
182
    /* HM eq. (5-36) (from BB, 1976) */
183
    const double c  =  cos(theta);
184
    const double s  =  sin(theta);
185
#else
186
485k
    const double b_div_a = 1 - P->f; // = P->b / P->a
187
485k
    const double p_div_a_b_div_a = p_div_a * b_div_a;
188
485k
    const double norm =
189
485k
        sqrt(z_div_a * z_div_a + p_div_a_b_div_a * p_div_a_b_div_a);
190
485k
    double c, s;
191
485k
    if (norm != 0) {
192
485k
        const double inv_norm = 1.0 / norm;
193
485k
        c = p_div_a_b_div_a * inv_norm;
194
485k
        s = z_div_a * inv_norm;
195
485k
    } else {
196
4
        c = 1;
197
4
        s = 0;
198
4
    }
199
485k
#endif
200
201
485k
    const double y_phi = z_div_a + P->e2s * b_div_a * s * s * s;
202
485k
    const double x_phi = p_div_a - P->es * c * c * c;
203
485k
    const double norm_phi = sqrt(y_phi * y_phi + x_phi * x_phi);
204
485k
    double cosphi, sinphi;
205
485k
    if (norm_phi != 0) {
206
485k
        const double inv_norm_phi = 1.0 / norm_phi;
207
485k
        cosphi = x_phi * inv_norm_phi;
208
485k
        sinphi = y_phi * inv_norm_phi;
209
485k
    } else {
210
0
        cosphi = 1;
211
0
        sinphi = 0;
212
0
    }
213
485k
    if (x_phi <= 0) {
214
        // this happen on non-sphere ellipsoid when x,y,z is very close to 0
215
        // there is no single solution to the cart->geodetic conversion in
216
        // that case, clamp to -90/90 deg and avoid a discontinuous boundary
217
        // near the poles
218
27
        lpz.phi = cart.z >= 0 ? M_HALFPI : -M_HALFPI;
219
27
        cosphi = 0;
220
27
        sinphi = cart.z >= 0 ? 1 : -1;
221
485k
    } else {
222
485k
        lpz.phi = atan(y_phi / x_phi);
223
485k
    }
224
485k
    lpz.lam = atan2(y_div_a, x_div_a);
225
226
485k
    if (cosphi < 1e-6) {
227
        /* poleward of 89.99994 deg, we avoid division by zero   */
228
        /* by computing the height as the cartesian z value      */
229
        /* minus the geocentric radius of the Earth at the given */
230
        /* latitude                                              */
231
81
        const double r = geocentric_radius(P->a, b_div_a, cosphi, sinphi);
232
81
        lpz.z = fabs(cart.z) - r;
233
485k
    } else {
234
485k
        const double N = normal_radius_of_curvature(P->a, P->es, sinphi);
235
485k
        lpz.z = P->a * p_div_a / cosphi - N;
236
485k
    }
237
238
485k
    return lpz;
239
485k
}
240
241
/* In effect, 2 cartesian coordinates of a point on the ellipsoid. Rather
242
 * pointless, but... */
243
0
static PJ_XY cart_forward(PJ_LP lp, PJ *P) {
244
0
    PJ_COORD point;
245
0
    point.lp = lp;
246
0
    point.lpz.z = 0;
247
248
0
    const auto xyz = cartesian(point.lpz, P);
249
0
    point.xyz = xyz;
250
0
    return point.xy;
251
0
}
252
253
/* And the other way round. Still rather pointless, but... */
254
0
static PJ_LP cart_reverse(PJ_XY xy, PJ *P) {
255
0
    PJ_COORD point;
256
0
    point.xy = xy;
257
0
    point.xyz.z = 0;
258
259
0
    const auto lpz = geodetic(point.xyz, P);
260
0
    point.lpz = lpz;
261
0
    return point.lp;
262
0
}
263
264
/*********************************************************************/
265
121k
PJ *PJ_CONVERSION(cart, 1) {
266
    /*********************************************************************/
267
121k
    P->fwd3d = cartesian;
268
121k
    P->inv3d = geodetic;
269
121k
    P->fwd = cart_forward;
270
121k
    P->inv = cart_reverse;
271
121k
    P->left = PJ_IO_UNITS_RADIANS;
272
121k
    P->right = PJ_IO_UNITS_CARTESIAN;
273
121k
    return P;
274
121k
}