Coverage Report

Created: 2025-08-29 07:11

/src/PROJ/src/projections/ortho.cpp
Line
Count
Source (jump to first uncovered line)
1
2
#include "proj.h"
3
#include "proj_internal.h"
4
#include <errno.h>
5
#include <math.h>
6
7
PROJ_HEAD(ortho, "Orthographic") "\n\tAzi, Sph&Ell";
8
9
namespace pj_ortho_ns {
10
enum Mode { N_POLE = 0, S_POLE = 1, EQUIT = 2, OBLIQ = 3 };
11
}
12
13
namespace { // anonymous namespace
14
struct pj_ortho_data {
15
    double sinph0;
16
    double cosph0;
17
    double nu0;
18
    double y_shift;
19
    double y_scale;
20
    enum pj_ortho_ns::Mode mode;
21
    double sinalpha;
22
    double cosalpha;
23
};
24
} // anonymous namespace
25
26
8.64k
#define EPS10 1.e-10
27
28
4.00k
static PJ_XY forward_error(PJ *P, PJ_LP lp, PJ_XY xy) {
29
4.00k
    proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
30
4.00k
    proj_log_trace(P,
31
4.00k
                   "Coordinate (%.3f, %.3f) is on the unprojected hemisphere",
32
4.00k
                   proj_todeg(lp.lam), proj_todeg(lp.phi));
33
4.00k
    return xy;
34
4.00k
}
35
36
0
static PJ_XY ortho_s_forward(PJ_LP lp, PJ *P) { /* Spheroidal, forward */
37
0
    PJ_XY xy;
38
0
    struct pj_ortho_data *Q = static_cast<struct pj_ortho_data *>(P->opaque);
39
0
    double coslam, cosphi, sinphi;
40
41
0
    xy.x = HUGE_VAL;
42
0
    xy.y = HUGE_VAL;
43
44
0
    cosphi = cos(lp.phi);
45
0
    coslam = cos(lp.lam);
46
0
    switch (Q->mode) {
47
0
    case pj_ortho_ns::EQUIT:
48
0
        if (cosphi * coslam < -EPS10)
49
0
            return forward_error(P, lp, xy);
50
0
        xy.y = sin(lp.phi);
51
0
        break;
52
0
    case pj_ortho_ns::OBLIQ:
53
0
        sinphi = sin(lp.phi);
54
55
        // Is the point visible from the projection plane ?
56
        // From
57
        // https://lists.osgeo.org/pipermail/proj/2020-September/009831.html
58
        // this is the dot product of the normal of the ellipsoid at the center
59
        // of the projection and at the point considered for projection.
60
        // [cos(phi)*cos(lambda), cos(phi)*sin(lambda), sin(phi)]
61
        // Also from Snyder's Map Projection - A working manual, equation (5-3),
62
        // page 149
63
0
        if (Q->sinph0 * sinphi + Q->cosph0 * cosphi * coslam < -EPS10)
64
0
            return forward_error(P, lp, xy);
65
0
        xy.y = Q->cosph0 * sinphi - Q->sinph0 * cosphi * coslam;
66
0
        break;
67
0
    case pj_ortho_ns::N_POLE:
68
0
        coslam = -coslam;
69
0
        PROJ_FALLTHROUGH;
70
0
    case pj_ortho_ns::S_POLE:
71
0
        if (fabs(lp.phi - P->phi0) - EPS10 > M_HALFPI)
72
0
            return forward_error(P, lp, xy);
73
0
        xy.y = cosphi * coslam;
74
0
        break;
75
0
    }
76
0
    xy.x = cosphi * sin(lp.lam);
77
78
0
    const double xp = xy.x;
79
0
    const double yp = xy.y;
80
0
    xy.x = (xp * Q->cosalpha - yp * Q->sinalpha) * P->k0;
81
0
    xy.y = (xp * Q->sinalpha + yp * Q->cosalpha) * P->k0;
82
0
    return xy;
83
0
}
84
85
0
static PJ_LP ortho_s_inverse(PJ_XY xy, PJ *P) { /* Spheroidal, inverse */
86
0
    PJ_LP lp;
87
0
    struct pj_ortho_data *Q = static_cast<struct pj_ortho_data *>(P->opaque);
88
0
    double sinc;
89
90
0
    lp.lam = HUGE_VAL;
91
0
    lp.phi = HUGE_VAL;
92
93
0
    const double xf = xy.x;
94
0
    const double yf = xy.y;
95
0
    xy.x = (Q->cosalpha * xf + Q->sinalpha * yf) / P->k0;
96
0
    xy.y = (-Q->sinalpha * xf + Q->cosalpha * yf) / P->k0;
97
98
0
    const double rh = hypot(xy.x, xy.y);
99
0
    sinc = rh;
100
0
    if (sinc > 1.) {
101
0
        if ((sinc - 1.) > EPS10) {
102
0
            proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
103
0
            return lp;
104
0
        }
105
0
        sinc = 1.;
106
0
    }
107
0
    const double cosc = sqrt(1. - sinc * sinc); /* in this range OK */
108
0
    if (fabs(rh) <= EPS10) {
109
0
        lp.phi = P->phi0;
110
0
        lp.lam = 0.0;
111
0
    } else {
112
0
        switch (Q->mode) {
113
0
        case pj_ortho_ns::N_POLE:
114
0
            xy.y = -xy.y;
115
0
            lp.phi = acos(sinc);
116
0
            break;
117
0
        case pj_ortho_ns::S_POLE:
118
0
            lp.phi = -acos(sinc);
119
0
            break;
120
0
        case pj_ortho_ns::EQUIT:
121
0
            lp.phi = xy.y * sinc / rh;
122
0
            xy.x *= sinc;
123
0
            xy.y = cosc * rh;
124
0
            goto sinchk;
125
0
        case pj_ortho_ns::OBLIQ:
126
0
            lp.phi = cosc * Q->sinph0 + xy.y * sinc * Q->cosph0 / rh;
127
0
            xy.y = (cosc - Q->sinph0 * lp.phi) * rh;
128
0
            xy.x *= sinc * Q->cosph0;
129
0
        sinchk:
130
0
            if (fabs(lp.phi) >= 1.)
131
0
                lp.phi = lp.phi < 0. ? -M_HALFPI : M_HALFPI;
132
0
            else
133
0
                lp.phi = asin(lp.phi);
134
0
            break;
135
0
        }
136
0
        lp.lam = (xy.y == 0. && (Q->mode == pj_ortho_ns::OBLIQ ||
137
0
                                 Q->mode == pj_ortho_ns::EQUIT))
138
0
                     ? (xy.x == 0.  ? 0.
139
0
                        : xy.x < 0. ? -M_HALFPI
140
0
                                    : M_HALFPI)
141
0
                     : atan2(xy.x, xy.y);
142
0
    }
143
0
    return lp;
144
0
}
145
146
8.31k
static PJ_XY ortho_e_forward(PJ_LP lp, PJ *P) { /* Ellipsoidal, forward */
147
8.31k
    PJ_XY xy;
148
8.31k
    struct pj_ortho_data *Q = static_cast<struct pj_ortho_data *>(P->opaque);
149
150
    // From EPSG guidance note 7.2, March 2020, §3.3.5 Orthographic
151
8.31k
    const double cosphi = cos(lp.phi);
152
8.31k
    const double sinphi = sin(lp.phi);
153
8.31k
    const double coslam = cos(lp.lam);
154
8.31k
    const double sinlam = sin(lp.lam);
155
156
    // Is the point visible from the projection plane ?
157
    // Same condition as in spherical case
158
8.31k
    if (Q->sinph0 * sinphi + Q->cosph0 * cosphi * coslam < -EPS10) {
159
4.00k
        xy.x = HUGE_VAL;
160
4.00k
        xy.y = HUGE_VAL;
161
4.00k
        return forward_error(P, lp, xy);
162
4.00k
    }
163
164
4.31k
    const double nu = 1.0 / sqrt(1.0 - P->es * sinphi * sinphi);
165
4.31k
    const double xp = nu * cosphi * sinlam;
166
4.31k
    const double yp = nu * (sinphi * Q->cosph0 - cosphi * Q->sinph0 * coslam) +
167
4.31k
                      P->es * (Q->nu0 * Q->sinph0 - nu * sinphi) * Q->cosph0;
168
4.31k
    xy.x = (Q->cosalpha * xp - Q->sinalpha * yp) * P->k0;
169
4.31k
    xy.y = (Q->sinalpha * xp + Q->cosalpha * yp) * P->k0;
170
171
4.31k
    return xy;
172
8.31k
}
173
174
0
static PJ_LP ortho_e_inverse(PJ_XY xy, PJ *P) { /* Ellipsoidal, inverse */
175
0
    PJ_LP lp;
176
0
    struct pj_ortho_data *Q = static_cast<struct pj_ortho_data *>(P->opaque);
177
178
0
    const auto SQ = [](double x) { return x * x; };
179
180
0
    const double xf = xy.x;
181
0
    const double yf = xy.y;
182
0
    xy.x = (Q->cosalpha * xf + Q->sinalpha * yf) / P->k0;
183
0
    xy.y = (-Q->sinalpha * xf + Q->cosalpha * yf) / P->k0;
184
185
0
    if (Q->mode == pj_ortho_ns::N_POLE || Q->mode == pj_ortho_ns::S_POLE) {
186
        // Polar case. Forward case equations can be simplified as:
187
        // xy.x = nu * cosphi * sinlam
188
        // xy.y = nu * -cosphi * coslam * sign(phi0)
189
        // ==> lam = atan2(xy.x, -xy.y * sign(phi0))
190
        // ==> xy.x^2 + xy.y^2 = nu^2 * cosphi^2
191
        //                rh^2 = cosphi^2 / (1 - es * sinphi^2)
192
        // ==>  cosphi^2 = rh^2 * (1 - es) / (1 - es * rh^2)
193
194
0
        const double rh2 = SQ(xy.x) + SQ(xy.y);
195
0
        if (rh2 >= 1. - 1e-15) {
196
0
            if ((rh2 - 1.) > EPS10) {
197
0
                proj_errno_set(
198
0
                    P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
199
0
                lp.lam = HUGE_VAL;
200
0
                lp.phi = HUGE_VAL;
201
0
                return lp;
202
0
            }
203
0
            lp.phi = 0;
204
0
        } else {
205
0
            lp.phi = acos(sqrt(rh2 * P->one_es / (1 - P->es * rh2))) *
206
0
                     (Q->mode == pj_ortho_ns::N_POLE ? 1 : -1);
207
0
        }
208
0
        lp.lam = atan2(xy.x, xy.y * (Q->mode == pj_ortho_ns::N_POLE ? -1 : 1));
209
0
        return lp;
210
0
    }
211
212
0
    if (Q->mode == pj_ortho_ns::EQUIT) {
213
        // Equatorial case. Forward case equations can be simplified as:
214
        // xy.x = nu * cosphi * sinlam
215
        // xy.y  = nu * sinphi * (1 - P->es)
216
        // x^2 * (1 - es * sinphi^2) = (1 - sinphi^2) * sinlam^2
217
        // y^2 / ((1 - es)^2 + y^2 * es) = sinphi^2
218
219
        // Equation of the ellipse
220
0
        if (SQ(xy.x) + SQ(xy.y * (P->a / P->b)) > 1 + 1e-11) {
221
0
            proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
222
0
            lp.lam = HUGE_VAL;
223
0
            lp.phi = HUGE_VAL;
224
0
            return lp;
225
0
        }
226
227
0
        const double sinphi2 =
228
0
            xy.y == 0 ? 0 : 1.0 / (SQ((1 - P->es) / xy.y) + P->es);
229
0
        if (sinphi2 > 1 - 1e-11) {
230
0
            lp.phi = M_PI_2 * (xy.y > 0 ? 1 : -1);
231
0
            lp.lam = 0;
232
0
            return lp;
233
0
        }
234
0
        lp.phi = asin(sqrt(sinphi2)) * (xy.y > 0 ? 1 : -1);
235
0
        const double sinlam =
236
0
            xy.x * sqrt((1 - P->es * sinphi2) / (1 - sinphi2));
237
0
        if (fabs(sinlam) - 1 > -1e-15)
238
0
            lp.lam = M_PI_2 * (xy.x > 0 ? 1 : -1);
239
0
        else
240
0
            lp.lam = asin(sinlam);
241
0
        return lp;
242
0
    }
243
244
    // Using Q->sinph0 * sinphi + Q->cosph0 * cosphi * coslam == 0 (visibity
245
    // condition of the forward case) in the forward equations, and a lot of
246
    // substitution games...
247
0
    PJ_XY xy_recentered;
248
0
    xy_recentered.x = xy.x;
249
0
    xy_recentered.y = (xy.y - Q->y_shift) / Q->y_scale;
250
0
    if (SQ(xy.x) + SQ(xy_recentered.y) > 1 + 1e-11) {
251
0
        proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
252
0
        lp.lam = HUGE_VAL;
253
0
        lp.phi = HUGE_VAL;
254
0
        return lp;
255
0
    }
256
257
    // From EPSG guidance note 7.2, March 2020, §3.3.5 Orthographic
258
259
    // It suggests as initial guess:
260
    // lp.lam = 0;
261
    // lp.phi = P->phi0;
262
    // But for poles, this will not converge well. Better use:
263
0
    lp = ortho_s_inverse(xy_recentered, P);
264
265
0
    for (int i = 0; i < 20; i++) {
266
0
        const double cosphi = cos(lp.phi);
267
0
        const double sinphi = sin(lp.phi);
268
0
        const double coslam = cos(lp.lam);
269
0
        const double sinlam = sin(lp.lam);
270
0
        const double one_minus_es_sinphi2 = 1.0 - P->es * sinphi * sinphi;
271
0
        const double nu = 1.0 / sqrt(one_minus_es_sinphi2);
272
0
        PJ_XY xy_new;
273
0
        xy_new.x = nu * cosphi * sinlam;
274
0
        xy_new.y = nu * (sinphi * Q->cosph0 - cosphi * Q->sinph0 * coslam) +
275
0
                   P->es * (Q->nu0 * Q->sinph0 - nu * sinphi) * Q->cosph0;
276
0
        const double rho = (1.0 - P->es) * nu / one_minus_es_sinphi2;
277
0
        const double J11 = -rho * sinphi * sinlam;
278
0
        const double J12 = nu * cosphi * coslam;
279
0
        const double J21 =
280
0
            rho * (cosphi * Q->cosph0 + sinphi * Q->sinph0 * coslam);
281
0
        const double J22 = nu * Q->sinph0 * cosphi * sinlam;
282
0
        const double D = J11 * J22 - J12 * J21;
283
0
        const double dx = xy.x - xy_new.x;
284
0
        const double dy = xy.y - xy_new.y;
285
0
        const double dphi = (J22 * dx - J12 * dy) / D;
286
0
        const double dlam = (-J21 * dx + J11 * dy) / D;
287
0
        lp.phi += dphi;
288
0
        if (lp.phi > M_PI_2) {
289
0
            lp.phi = M_PI_2 - (lp.phi - M_PI_2);
290
0
            lp.lam = adjlon(lp.lam + M_PI);
291
0
        } else if (lp.phi < -M_PI_2) {
292
0
            lp.phi = -M_PI_2 + (-M_PI_2 - lp.phi);
293
0
            lp.lam = adjlon(lp.lam + M_PI);
294
0
        }
295
0
        lp.lam += dlam;
296
0
        if (fabs(dphi) < 1e-12 && fabs(dlam) < 1e-12) {
297
0
            return lp;
298
0
        }
299
0
    }
300
0
    proj_context_errno_set(P->ctx,
301
0
                           PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN);
302
0
    return lp;
303
0
}
304
305
165
PJ *PJ_PROJECTION(ortho) {
306
165
    struct pj_ortho_data *Q = static_cast<struct pj_ortho_data *>(
307
165
        calloc(1, sizeof(struct pj_ortho_data)));
308
165
    if (nullptr == Q)
309
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
310
165
    P->opaque = Q;
311
312
165
    Q->sinph0 = sin(P->phi0);
313
165
    Q->cosph0 = cos(P->phi0);
314
165
    if (fabs(fabs(P->phi0) - M_HALFPI) <= EPS10)
315
0
        Q->mode = P->phi0 < 0. ? pj_ortho_ns::S_POLE : pj_ortho_ns::N_POLE;
316
165
    else if (fabs(P->phi0) > EPS10) {
317
18
        Q->mode = pj_ortho_ns::OBLIQ;
318
18
    } else
319
147
        Q->mode = pj_ortho_ns::EQUIT;
320
165
    if (P->es == 0) {
321
1
        P->inv = ortho_s_inverse;
322
1
        P->fwd = ortho_s_forward;
323
164
    } else {
324
164
        Q->nu0 = 1.0 / sqrt(1.0 - P->es * Q->sinph0 * Q->sinph0);
325
164
        Q->y_shift = P->es * Q->nu0 * Q->sinph0 * Q->cosph0;
326
164
        Q->y_scale = 1.0 / sqrt(1.0 - P->es * Q->cosph0 * Q->cosph0);
327
164
        P->inv = ortho_e_inverse;
328
164
        P->fwd = ortho_e_forward;
329
164
    }
330
331
165
    const double alpha = pj_param(P->ctx, P->params, "ralpha").f;
332
165
    Q->sinalpha = sin(alpha);
333
165
    Q->cosalpha = cos(alpha);
334
335
165
    return P;
336
165
}
337
338
#undef EPS10