Coverage Report

Created: 2025-08-28 06:57

/src/proj/src/conversions/cart.cpp
Line
Count
Source (jump to first uncovered line)
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
220
static double normal_radius_of_curvature(double a, double es, double sinphi) {
107
    /*********************************************************************/
108
220
    if (es == 0)
109
44
        return a;
110
    /* This is from WP.  HM formula 2-149 gives an a,b version */
111
176
    return a / sqrt(1 - es * sinphi * sinphi);
112
220
}
113
114
/*********************************************************************/
115
static double geocentric_radius(double a, double b_div_a, double cosphi,
116
42
                                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
42
    const double cosphi_squared = cosphi * cosphi;
129
42
    const double sinphi_squared = sinphi * sinphi;
130
42
    const double b_div_a_squared = b_div_a * b_div_a;
131
42
    const double b_div_a_squared_mul_sinphi_squared =
132
42
        b_div_a_squared * sinphi_squared;
133
42
    return a * sqrt((cosphi_squared +
134
42
                     b_div_a_squared * b_div_a_squared_mul_sinphi_squared) /
135
42
                    (cosphi_squared + b_div_a_squared_mul_sinphi_squared));
136
42
}
137
138
/*********************************************************************/
139
220
static PJ_XYZ cartesian(PJ_LPZ geod, PJ *P) {
140
    /*********************************************************************/
141
220
    PJ_XYZ xyz;
142
143
220
    const double cosphi = cos(geod.phi);
144
220
    const double sinphi = sin(geod.phi);
145
220
    const double N = normal_radius_of_curvature(P->a, P->es, sinphi);
146
147
    /* HM formula 5-27 (z formula follows WP) */
148
220
    xyz.x = (N + geod.z) * cosphi * cos(geod.lam);
149
220
    xyz.y = (N + geod.z) * cosphi * sin(geod.lam);
150
220
    xyz.z = (N * (1 - P->es) + geod.z) * sinphi;
151
152
220
    return xyz;
153
220
}
154
155
/*********************************************************************/
156
42
static PJ_LPZ geodetic(PJ_XYZ cart, PJ *P) {
157
    /*********************************************************************/
158
42
    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
42
    const double x_div_a = cart.x * P->ra;
171
42
    const double y_div_a = cart.y * P->ra;
172
42
    const double z_div_a = cart.z * P->ra;
173
42
#endif
174
175
    /* Perpendicular distance from point to Z-axis (HM eq. 5-28) */
176
42
    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
42
    const double b_div_a = 1 - P->f; // = P->b / P->a
187
42
    const double p_div_a_b_div_a = p_div_a * b_div_a;
188
42
    const double norm =
189
42
        sqrt(z_div_a * z_div_a + p_div_a_b_div_a * p_div_a_b_div_a);
190
42
    double c, s;
191
42
    if (norm != 0) {
192
0
        const double inv_norm = 1.0 / norm;
193
0
        c = p_div_a_b_div_a * inv_norm;
194
0
        s = z_div_a * inv_norm;
195
42
    } else {
196
42
        c = 1;
197
42
        s = 0;
198
42
    }
199
42
#endif
200
201
42
    const double y_phi = z_div_a + P->e2s * b_div_a * s * s * s;
202
42
    const double x_phi = p_div_a - P->es * c * c * c;
203
42
    const double norm_phi = sqrt(y_phi * y_phi + x_phi * x_phi);
204
42
    double cosphi, sinphi;
205
42
    if (norm_phi != 0) {
206
42
        const double inv_norm_phi = 1.0 / norm_phi;
207
42
        cosphi = x_phi * inv_norm_phi;
208
42
        sinphi = y_phi * inv_norm_phi;
209
42
    } else {
210
0
        cosphi = 1;
211
0
        sinphi = 0;
212
0
    }
213
42
    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
42
        lpz.phi = cart.z >= 0 ? M_HALFPI : -M_HALFPI;
219
42
        cosphi = 0;
220
42
        sinphi = cart.z >= 0 ? 1 : -1;
221
42
    } else {
222
0
        lpz.phi = atan(y_phi / x_phi);
223
0
    }
224
42
    lpz.lam = atan2(y_div_a, x_div_a);
225
226
42
    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
42
        const double r = geocentric_radius(P->a, b_div_a, cosphi, sinphi);
232
42
        lpz.z = fabs(cart.z) - r;
233
42
    } else {
234
0
        const double N = normal_radius_of_curvature(P->a, P->es, sinphi);
235
0
        lpz.z = P->a * p_div_a / cosphi - N;
236
0
    }
237
238
42
    return lpz;
239
42
}
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
23.4k
PJ *PJ_CONVERSION(cart, 1) {
266
    /*********************************************************************/
267
23.4k
    P->fwd3d = cartesian;
268
23.4k
    P->inv3d = geodetic;
269
23.4k
    P->fwd = cart_forward;
270
23.4k
    P->inv = cart_reverse;
271
23.4k
    P->left = PJ_IO_UNITS_RADIANS;
272
23.4k
    P->right = PJ_IO_UNITS_CARTESIAN;
273
23.4k
    return P;
274
23.4k
}