Coverage Report

Created: 2025-07-23 09:13

/src/gdal/proj/src/conversions/topocentric.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 * Project:  PROJ
3
 * Purpose:  Convert between geocentric coordinates and topocentric (ENU)
4
 *coordinates
5
 *
6
 ******************************************************************************
7
 * Copyright (c) 2020, Even Rouault <even.rouault at spatialys.com>
8
 *
9
 * Permission is hereby granted, free of charge, to any person obtaining a
10
 * copy of this software and associated documentation files (the "Software"),
11
 * to deal in the Software without restriction, including without limitation
12
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13
 * and/or sell copies of the Software, and to permit persons to whom the
14
 * Software is furnished to do so, subject to the following conditions:
15
 *
16
 * The above copyright notice and this permission notice shall be included
17
 * in all copies or substantial portions of the Software.
18
 *
19
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25
 * DEALINGS IN THE SOFTWARE.
26
 *****************************************************************************/
27
28
#include "proj_internal.h"
29
#include <errno.h>
30
#include <math.h>
31
32
PROJ_HEAD(topocentric, "Geocentric/Topocentric conversion");
33
34
// Notations and formulas taken from IOGP Publication 373-7-2 -
35
// Geomatics Guidance Note number 7, part 2 - October 2020
36
37
namespace { // anonymous namespace
38
struct pj_opaque {
39
    double X0;
40
    double Y0;
41
    double Z0;
42
    double sinphi0;
43
    double cosphi0;
44
    double sinlam0;
45
    double coslam0;
46
};
47
} // anonymous namespace
48
49
// Convert from geocentric to topocentric
50
0
static void topocentric_fwd(PJ_COORD &coo, PJ *P) {
51
0
    struct pj_opaque *Q = static_cast<struct pj_opaque *>(P->opaque);
52
0
    const double dX = coo.xyz.x - Q->X0;
53
0
    const double dY = coo.xyz.y - Q->Y0;
54
0
    const double dZ = coo.xyz.z - Q->Z0;
55
0
    coo.xyz.x = -dX * Q->sinlam0 + dY * Q->coslam0;
56
0
    coo.xyz.y = -dX * Q->sinphi0 * Q->coslam0 - dY * Q->sinphi0 * Q->sinlam0 +
57
0
                dZ * Q->cosphi0;
58
0
    coo.xyz.z = dX * Q->cosphi0 * Q->coslam0 + dY * Q->cosphi0 * Q->sinlam0 +
59
0
                dZ * Q->sinphi0;
60
0
}
61
62
// Convert from topocentric to geocentric
63
0
static void topocentric_inv(PJ_COORD &coo, PJ *P) {
64
0
    struct pj_opaque *Q = static_cast<struct pj_opaque *>(P->opaque);
65
0
    const double x = coo.xyz.x;
66
0
    const double y = coo.xyz.y;
67
0
    const double z = coo.xyz.z;
68
0
    coo.xyz.x = Q->X0 - x * Q->sinlam0 - y * Q->sinphi0 * Q->coslam0 +
69
0
                z * Q->cosphi0 * Q->coslam0;
70
0
    coo.xyz.y = Q->Y0 + x * Q->coslam0 - y * Q->sinphi0 * Q->sinlam0 +
71
0
                z * Q->cosphi0 * Q->sinlam0;
72
0
    coo.xyz.z = Q->Z0 + y * Q->cosphi0 + z * Q->sinphi0;
73
0
}
74
75
/*********************************************************************/
76
17.5k
PJ *PJ_CONVERSION(topocentric, 1) {
77
    /*********************************************************************/
78
17.5k
    struct pj_opaque *Q =
79
17.5k
        static_cast<struct pj_opaque *>(calloc(1, sizeof(struct pj_opaque)));
80
17.5k
    if (nullptr == Q)
81
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
82
17.5k
    P->opaque = static_cast<void *>(Q);
83
84
    // The topocentric origin can be specified either in geocentric coordinates
85
    // (X_0,Y_0,Z_0) or as geographic coordinates (lon_0,lat_0,h_0)
86
    // Checks:
87
    // - X_0 or lon_0 must be specified
88
    // - If X_0 is specified, the Y_0 and Z_0 must also be
89
    // - If lon_0 is specified, then lat_0 must also be
90
    // - If any of X_0, Y_0, Z_0 is specified, then any of lon_0,lat_0,h_0 must
91
    //   not be, and vice versa.
92
17.5k
    const auto hasX0 = pj_param_exists(P->params, "X_0");
93
17.5k
    const auto hasY0 = pj_param_exists(P->params, "Y_0");
94
17.5k
    const auto hasZ0 = pj_param_exists(P->params, "Z_0");
95
17.5k
    const auto hasLon0 = pj_param_exists(P->params, "lon_0");
96
17.5k
    const auto hasLat0 = pj_param_exists(P->params, "lat_0");
97
17.5k
    const auto hash0 = pj_param_exists(P->params, "h_0");
98
17.5k
    if (!hasX0 && !hasLon0) {
99
988
        proj_log_error(P, _("missing X_0 or lon_0"));
100
988
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
101
988
    }
102
16.5k
    if ((hasX0 || hasY0 || hasZ0) && (hasLon0 || hasLat0 || hash0)) {
103
755
        proj_log_error(
104
755
            P, _("(X_0,Y_0,Z_0) and (lon_0,lat_0,h_0) are mutually exclusive"));
105
755
        return pj_default_destructor(
106
755
            P, PROJ_ERR_INVALID_OP_MUTUALLY_EXCLUSIVE_ARGS);
107
755
    }
108
15.8k
    if (hasX0 && (!hasY0 || !hasZ0)) {
109
657
        proj_log_error(P, _("missing Y_0 and/or Z_0"));
110
657
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
111
657
    }
112
15.1k
    if (hasLon0 && !hasLat0) // allow missing h_0
113
493
    {
114
493
        proj_log_error(P, _("missing lat_0"));
115
493
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
116
493
    }
117
118
    // Pass a dummy ellipsoid definition that will be overridden just afterwards
119
14.6k
    PJ *cart = proj_create(P->ctx, "+proj=cart +a=1");
120
14.6k
    if (cart == nullptr)
121
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
122
    /* inherit ellipsoid definition from P to cart */
123
14.6k
    pj_inherit_ellipsoid_def(P, cart);
124
125
14.6k
    if (hasX0) {
126
486
        Q->X0 = pj_param(P->ctx, P->params, "dX_0").f;
127
486
        Q->Y0 = pj_param(P->ctx, P->params, "dY_0").f;
128
486
        Q->Z0 = pj_param(P->ctx, P->params, "dZ_0").f;
129
130
        // Compute lam0, phi0 from X0,Y0,Z0
131
486
        PJ_XYZ xyz;
132
486
        xyz.x = Q->X0;
133
486
        xyz.y = Q->Y0;
134
486
        xyz.z = Q->Z0;
135
486
        const auto lpz = pj_inv3d(xyz, cart);
136
486
        Q->sinphi0 = sin(lpz.phi);
137
486
        Q->cosphi0 = cos(lpz.phi);
138
486
        Q->sinlam0 = sin(lpz.lam);
139
486
        Q->coslam0 = cos(lpz.lam);
140
14.2k
    } else {
141
        // Compute X0,Y0,Z0 from lam0, phi0, h0
142
14.2k
        PJ_LPZ lpz;
143
14.2k
        lpz.lam = P->lam0;
144
14.2k
        lpz.phi = P->phi0;
145
14.2k
        lpz.z = pj_param(P->ctx, P->params, "dh_0").f;
146
14.2k
        const auto xyz = pj_fwd3d(lpz, cart);
147
14.2k
        Q->X0 = xyz.x;
148
14.2k
        Q->Y0 = xyz.y;
149
14.2k
        Q->Z0 = xyz.z;
150
151
14.2k
        Q->sinphi0 = sin(P->phi0);
152
14.2k
        Q->cosphi0 = cos(P->phi0);
153
14.2k
        Q->sinlam0 = sin(P->lam0);
154
14.2k
        Q->coslam0 = cos(P->lam0);
155
14.2k
    }
156
157
14.6k
    proj_destroy(cart);
158
159
14.6k
    P->fwd4d = topocentric_fwd;
160
14.6k
    P->inv4d = topocentric_inv;
161
14.6k
    P->left = PJ_IO_UNITS_CARTESIAN;
162
14.6k
    P->right = PJ_IO_UNITS_CARTESIAN;
163
14.6k
    return P;
164
14.6k
}