Coverage Report

Created: 2025-07-12 06:32

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