Coverage Report

Created: 2025-07-18 07:18

/src/PROJ/src/transformations/helmert.cpp
Line
Count
Source (jump to first uncovered line)
1
/***********************************************************************
2
3
             3-, 4-and 7-parameter shifts, and their 6-, 8-
4
                and 14-parameter kinematic counterparts.
5
6
                    Thomas Knudsen, 2016-05-24
7
8
************************************************************************
9
10
  Implements 3(6)-, 4(8) and 7(14)-parameter Helmert transformations for
11
  3D data.
12
13
  Also incorporates Molodensky-Badekas variant of 7-parameter Helmert
14
  transformation, where the rotation is not applied regarding the centre
15
  of the spheroid, but given a reference point.
16
17
  Primarily useful for implementation of datum shifts in transformation
18
  pipelines.
19
20
************************************************************************
21
22
Thomas Knudsen, thokn@sdfe.dk, 2016-05-24/06-05
23
Kristian Evers, kreve@sdfe.dk, 2017-05-01
24
Even Rouault, even.rouault@spatialys.com
25
Last update: 2018-10-26
26
27
************************************************************************
28
* Copyright (c) 2016, Thomas Knudsen / SDFE
29
*
30
* Permission is hereby granted, free of charge, to any person obtaining a
31
* copy of this software and associated documentation files (the "Software"),
32
* to deal in the Software without restriction, including without limitation
33
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
34
* and/or sell copies of the Software, and to permit persons to whom the
35
* Software is furnished to do so, subject to the following conditions:
36
*
37
* The above copyright notice and this permission notice shall be included
38
* in all copies or substantial portions of the Software.
39
*
40
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
41
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
43
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
45
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
46
* DEALINGS IN THE SOFTWARE.
47
*
48
***********************************************************************/
49
50
#include <errno.h>
51
#include <math.h>
52
53
#include "proj_internal.h"
54
55
PROJ_HEAD(helmert, "3(6)-, 4(8)- and 7(14)-parameter Helmert shift");
56
PROJ_HEAD(molobadekas, "Molodensky-Badekas transformation");
57
58
static PJ_XYZ helmert_forward_3d(PJ_LPZ lpz, PJ *P);
59
static PJ_LPZ helmert_reverse_3d(PJ_XYZ xyz, PJ *P);
60
61
/***********************************************************************/
62
namespace { // anonymous namespace
63
struct pj_opaque_helmert {
64
    /************************************************************************
65
        Projection specific elements for the "helmert" PJ object
66
    ************************************************************************/
67
    PJ_XYZ xyz;
68
    PJ_XYZ xyz_0;
69
    PJ_XYZ dxyz;
70
    PJ_XYZ refp;
71
    PJ_OPK opk;
72
    PJ_OPK opk_0;
73
    PJ_OPK dopk;
74
    double scale;
75
    double scale_0;
76
    double dscale;
77
    double theta;
78
    double theta_0;
79
    double dtheta;
80
    double R[3][3];
81
    double t_epoch, t_obs;
82
    int no_rotation, exact, fourparam;
83
    int is_position_vector; /* 1 = position_vector, 0 = coordinate_frame */
84
};
85
} // anonymous namespace
86
87
/* Make the maths of the rotation operations somewhat more readable and textbook
88
 * like */
89
62.2k
#define R00 (Q->R[0][0])
90
66.1k
#define R01 (Q->R[0][1])
91
66.1k
#define R02 (Q->R[0][2])
92
93
66.1k
#define R10 (Q->R[1][0])
94
62.2k
#define R11 (Q->R[1][1])
95
66.1k
#define R12 (Q->R[1][2])
96
97
66.1k
#define R20 (Q->R[2][0])
98
66.1k
#define R21 (Q->R[2][1])
99
62.2k
#define R22 (Q->R[2][2])
100
101
/**************************************************************************/
102
52.5k
static void update_parameters(PJ *P) {
103
    /***************************************************************************
104
105
        Update transformation parameters.
106
        ---------------------------------
107
108
        The 14-parameter Helmert transformation is at it's core the same as the
109
        7-parameter transformation, since the transformation parameters are
110
        projected forward or backwards in time via the rate of changes of the
111
        parameters. The transformation parameters are calculated for a specific
112
        epoch before the actual Helmert transformation is carried out.
113
114
        The transformation parameters are updated with the following equation
115
    [0]:
116
117
                          .
118
        P(t) = P(EPOCH) + P * (t - EPOCH)
119
120
                                                                  .
121
        where EPOCH is the epoch indicated in the above table and P is the rate
122
        of that parameter.
123
124
        [0] http://itrf.ign.fr/doc_ITRF/Transfo-ITRF2008_ITRFs.txt
125
126
    *******************************************************************************/
127
128
52.5k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
129
52.5k
    double dt = Q->t_obs - Q->t_epoch;
130
131
52.5k
    Q->xyz.x = Q->xyz_0.x + Q->dxyz.x * dt;
132
52.5k
    Q->xyz.y = Q->xyz_0.y + Q->dxyz.y * dt;
133
52.5k
    Q->xyz.z = Q->xyz_0.z + Q->dxyz.z * dt;
134
135
52.5k
    Q->opk.o = Q->opk_0.o + Q->dopk.o * dt;
136
52.5k
    Q->opk.p = Q->opk_0.p + Q->dopk.p * dt;
137
52.5k
    Q->opk.k = Q->opk_0.k + Q->dopk.k * dt;
138
139
52.5k
    Q->scale = Q->scale_0 + Q->dscale * dt;
140
141
52.5k
    Q->theta = Q->theta_0 + Q->dtheta * dt;
142
143
    /* debugging output */
144
52.5k
    if (proj_log_level(P->ctx, PJ_LOG_TELL) >= PJ_LOG_TRACE) {
145
0
        proj_log_trace(P,
146
0
                       "Transformation parameters for observation "
147
0
                       "t_obs=%g (t_epoch=%g):",
148
0
                       Q->t_obs, Q->t_epoch);
149
0
        proj_log_trace(P, "x: %g", Q->xyz.x);
150
0
        proj_log_trace(P, "y: %g", Q->xyz.y);
151
0
        proj_log_trace(P, "z: %g", Q->xyz.z);
152
0
        proj_log_trace(P, "s: %g", Q->scale * 1e-6);
153
0
        proj_log_trace(P, "rx: %g", Q->opk.o);
154
0
        proj_log_trace(P, "ry: %g", Q->opk.p);
155
0
        proj_log_trace(P, "rz: %g", Q->opk.k);
156
0
        proj_log_trace(P, "theta: %g", Q->theta);
157
0
    }
158
52.5k
}
159
160
/**************************************************************************/
161
52.5k
static void build_rot_matrix(PJ *P) {
162
    /***************************************************************************
163
164
        Build rotation matrix.
165
        ----------------------
166
167
        Here we rename rotation indices from omega, phi, kappa (opk), to
168
        fi (i.e. phi), theta, psi (ftp), in order to reduce the mental agility
169
        needed to implement the expression for the rotation matrix derived over
170
        at https://en.wikipedia.org/wiki/Rotation_formalisms_in_three_dimensions
171
        The relevant section is Euler angles ( z-’-x" intrinsic) -> Rotation
172
    matrix
173
174
        By default small angle approximations are used:
175
        The matrix elements are approximated by expanding the trigonometric
176
        functions to linear order (i.e. cos(x) = 1, sin(x) = x), and discarding
177
        products of second order.
178
179
        This was a useful hack when calculating by hand was the only option,
180
        but in general, today, should be avoided because:
181
182
        1. It does not save much computation time, as the rotation matrix
183
           is built only once and probably used many times (except when
184
           transforming spatio-temporal coordinates).
185
186
        2. The error induced may be too large for ultra high accuracy
187
           applications: the Earth is huge and the linear error is
188
           approximately the angular error multiplied by the Earth radius.
189
190
        However, in many cases the approximation is necessary, since it has
191
        been used historically: Rotation angles from older published datum
192
        shifts may actually be a least squares fit to the linearized rotation
193
        approximation, hence not being strictly valid for deriving the exact
194
        rotation matrix. In fact, most publicly available transformation
195
        parameters are based on the approximate Helmert transform, which is why
196
        we use that as the default setting, even though it is more correct to
197
        use the exact form of the equations.
198
199
        So in order to fit historically derived coordinates, the access to
200
        the approximate rotation matrix is necessary - at least in principle.
201
202
        Also, when using any published datum transformation information, one
203
        should always check which convention (exact or approximate rotation
204
        matrix) is expected, and whether the induced error for selecting
205
        the opposite convention is acceptable (which it often is).
206
207
208
        Sign conventions
209
        ----------------
210
211
        Take care: Two different sign conventions exist for the rotation terms.
212
213
        Conceptually they relate to whether we rotate the coordinate system
214
        or the "position vector" (the vector going from the coordinate system
215
        origin to the point being transformed, i.e. the point coordinates
216
        interpreted as vector coordinates).
217
218
        Switching between the "position vector" and "coordinate system"
219
        conventions is simply a matter of switching the sign of the rotation
220
        angles, which algebraically also translates into a transposition of
221
        the rotation matrix.
222
223
        Hence, as geodetic constants should preferably be referred to exactly
224
        as published, the "convention" option provides the ability to switch
225
        between the conventions.
226
227
    ***************************************************************************/
228
52.5k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
229
230
52.5k
    double f, t, p;    /* phi/fi , theta, psi  */
231
52.5k
    double cf, ct, cp; /* cos (fi, theta, psi) */
232
52.5k
    double sf, st, sp; /* sin (fi, theta, psi) */
233
234
    /* rename   (omega, phi, kappa)   to   (fi, theta, psi)   */
235
52.5k
    f = Q->opk.o;
236
52.5k
    t = Q->opk.p;
237
52.5k
    p = Q->opk.k;
238
239
    /* Those equations are given assuming coordinate frame convention. */
240
    /* For the position vector convention, we transpose the matrix just after.
241
     */
242
52.5k
    if (Q->exact) {
243
34.6k
        cf = cos(f);
244
34.6k
        sf = sin(f);
245
34.6k
        ct = cos(t);
246
34.6k
        st = sin(t);
247
34.6k
        cp = cos(p);
248
34.6k
        sp = sin(p);
249
250
34.6k
        R00 = ct * cp;
251
34.6k
        R01 = cf * sp + sf * st * cp;
252
34.6k
        R02 = sf * sp - cf * st * cp;
253
254
34.6k
        R10 = -ct * sp;
255
34.6k
        R11 = cf * cp - sf * st * sp;
256
34.6k
        R12 = sf * cp + cf * st * sp;
257
258
34.6k
        R20 = st;
259
34.6k
        R21 = -sf * ct;
260
34.6k
        R22 = cf * ct;
261
34.6k
    } else {
262
17.9k
        R00 = 1;
263
17.9k
        R01 = p;
264
17.9k
        R02 = -t;
265
266
17.9k
        R10 = -p;
267
17.9k
        R11 = 1;
268
17.9k
        R12 = f;
269
270
17.9k
        R20 = t;
271
17.9k
        R21 = -f;
272
17.9k
        R22 = 1;
273
17.9k
    }
274
275
    /*
276
        For comparison: Description from Engsager/Poder implementation
277
        in set_dtm_1.c (trlib)
278
279
        DATUM SHIFT:
280
        TO = scale * ROTZ * ROTY * ROTX * FROM + TRANSLA
281
282
             ( cz sz 0)         (cy 0 -sy)         (1   0  0)
283
        ROTZ=(-sz cz 0),   ROTY=(0  1   0),   ROTX=(0  cx sx)
284
             (  0  0 1)         (sy 0  cy)         (0 -sx cx)
285
286
        trp->r11  =  cos_ry*cos_rz;
287
        trp->r12  =  cos_rx*sin_rz + sin_rx*sin_ry*cos_rz;
288
        trp->r13  =  sin_rx*sin_rz - cos_rx*sin_ry*cos_rz;
289
290
        trp->r21  = -cos_ry*sin_rz;
291
        trp->r22  =  cos_rx*cos_rz - sin_rx*sin_ry*sin_rz;
292
        trp->r23  =  sin_rx*cos_rz + cos_rx*sin_ry*sin_rz;
293
294
        trp->r31  =  sin_ry;
295
        trp->r32  = -sin_rx*cos_ry;
296
        trp->r33  =  cos_rx*cos_ry;
297
298
        trp->scale = 1.0 + scale;
299
    */
300
301
52.5k
    if (Q->is_position_vector) {
302
1.91k
        double r;
303
1.91k
        r = R01;
304
1.91k
        R01 = R10;
305
1.91k
        R10 = r;
306
1.91k
        r = R02;
307
1.91k
        R02 = R20;
308
1.91k
        R20 = r;
309
1.91k
        r = R12;
310
1.91k
        R12 = R21;
311
1.91k
        R21 = r;
312
1.91k
    }
313
314
    /* some debugging output */
315
52.5k
    if (proj_log_level(P->ctx, PJ_LOG_TELL) >= PJ_LOG_TRACE) {
316
0
        proj_log_trace(P, "Rotation Matrix:");
317
0
        proj_log_trace(P, "  | % 6.6g  % 6.6g  % 6.6g |", R00, R01, R02);
318
0
        proj_log_trace(P, "  | % 6.6g  % 6.6g  % 6.6g |", R10, R11, R12);
319
0
        proj_log_trace(P, "  | % 6.6g  % 6.6g  % 6.6g |", R20, R21, R22);
320
0
    }
321
52.5k
}
322
323
/***********************************************************************/
324
0
static PJ_XY helmert_forward(PJ_LP lp, PJ *P) {
325
    /***********************************************************************/
326
0
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
327
0
    PJ_COORD point = {{0, 0, 0, 0}};
328
0
    double x, y, cr, sr;
329
0
    point.lp = lp;
330
331
0
    cr = cos(Q->theta) * Q->scale;
332
0
    sr = sin(Q->theta) * Q->scale;
333
0
    x = point.xy.x;
334
0
    y = point.xy.y;
335
336
0
    point.xy.x = cr * x + sr * y + Q->xyz_0.x;
337
0
    point.xy.y = -sr * x + cr * y + Q->xyz_0.y;
338
339
0
    return point.xy;
340
0
}
341
342
/***********************************************************************/
343
0
static PJ_LP helmert_reverse(PJ_XY xy, PJ *P) {
344
    /***********************************************************************/
345
0
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
346
0
    PJ_COORD point = {{0, 0, 0, 0}};
347
0
    double x, y, sr, cr;
348
0
    point.xy = xy;
349
350
0
    cr = cos(Q->theta) / Q->scale;
351
0
    sr = sin(Q->theta) / Q->scale;
352
0
    x = point.xy.x - Q->xyz_0.x;
353
0
    y = point.xy.y - Q->xyz_0.y;
354
355
0
    point.xy.x = x * cr - y * sr;
356
0
    point.xy.y = x * sr + y * cr;
357
358
0
    return point.lp;
359
0
}
360
361
/***********************************************************************/
362
162k
static PJ_XYZ helmert_forward_3d(PJ_LPZ lpz, PJ *P) {
363
    /***********************************************************************/
364
162k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
365
162k
    PJ_COORD point = {{0, 0, 0, 0}};
366
162k
    double X, Y, Z, scale;
367
368
162k
    point.lpz = lpz;
369
370
162k
    if (Q->fourparam) {
371
0
        const auto xy = helmert_forward(point.lp, P);
372
0
        point.xy = xy;
373
0
        return point.xyz;
374
0
    }
375
376
162k
    if (Q->no_rotation && Q->scale == 0) {
377
159k
        point.xyz.x = lpz.lam + Q->xyz.x;
378
159k
        point.xyz.y = lpz.phi + Q->xyz.y;
379
159k
        point.xyz.z = lpz.z + Q->xyz.z;
380
159k
        return point.xyz;
381
159k
    }
382
383
3.19k
    scale = 1 + Q->scale * 1e-6;
384
385
3.19k
    X = lpz.lam - Q->refp.x;
386
3.19k
    Y = lpz.phi - Q->refp.y;
387
3.19k
    Z = lpz.z - Q->refp.z;
388
389
3.19k
    point.xyz.x = scale * (R00 * X + R01 * Y + R02 * Z);
390
3.19k
    point.xyz.y = scale * (R10 * X + R11 * Y + R12 * Z);
391
3.19k
    point.xyz.z = scale * (R20 * X + R21 * Y + R22 * Z);
392
393
3.19k
    point.xyz.x += Q->xyz.x; /* for Molodensky-Badekas, Q->xyz already
394
                                incorporates the Q->refp offset */
395
3.19k
    point.xyz.y += Q->xyz.y;
396
3.19k
    point.xyz.z += Q->xyz.z;
397
398
3.19k
    return point.xyz;
399
162k
}
400
401
/***********************************************************************/
402
261k
static PJ_LPZ helmert_reverse_3d(PJ_XYZ xyz, PJ *P) {
403
    /***********************************************************************/
404
261k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
405
261k
    PJ_COORD point = {{0, 0, 0, 0}};
406
261k
    double X, Y, Z, scale;
407
408
261k
    point.xyz = xyz;
409
410
261k
    if (Q->fourparam) {
411
0
        const auto lp = helmert_reverse(point.xy, P);
412
0
        point.lp = lp;
413
0
        return point.lpz;
414
0
    }
415
416
261k
    if (Q->no_rotation && Q->scale == 0) {
417
254k
        point.xyz.x = xyz.x - Q->xyz.x;
418
254k
        point.xyz.y = xyz.y - Q->xyz.y;
419
254k
        point.xyz.z = xyz.z - Q->xyz.z;
420
254k
        return point.lpz;
421
254k
    }
422
423
6.55k
    scale = 1 + Q->scale * 1e-6;
424
425
    /* Unscale and deoffset */
426
6.55k
    X = (xyz.x - Q->xyz.x) / scale;
427
6.55k
    Y = (xyz.y - Q->xyz.y) / scale;
428
6.55k
    Z = (xyz.z - Q->xyz.z) / scale;
429
430
    /* Inverse rotation through transpose multiplication */
431
6.55k
    point.xyz.x = (R00 * X + R10 * Y + R20 * Z) + Q->refp.x;
432
6.55k
    point.xyz.y = (R01 * X + R11 * Y + R21 * Z) + Q->refp.y;
433
6.55k
    point.xyz.z = (R02 * X + R12 * Y + R22 * Z) + Q->refp.z;
434
435
6.55k
    return point.lpz;
436
261k
}
437
438
162k
static void helmert_forward_4d(PJ_COORD &point, PJ *P) {
439
162k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
440
441
    /* We only need to rebuild the rotation matrix if the
442
     * observation time is different from the last call */
443
162k
    double t_obs = (point.xyzt.t == HUGE_VAL) ? Q->t_epoch : point.xyzt.t;
444
162k
    if (t_obs != Q->t_obs) {
445
0
        Q->t_obs = t_obs;
446
0
        update_parameters(P);
447
0
        build_rot_matrix(P);
448
0
    }
449
450
    // Assigning in 2 steps avoids cppcheck warning
451
    // "Overlapping read/write of union is undefined behavior"
452
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
453
162k
    const auto xyz = helmert_forward_3d(point.lpz, P);
454
162k
    point.xyz = xyz;
455
162k
}
456
457
261k
static void helmert_reverse_4d(PJ_COORD &point, PJ *P) {
458
261k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
459
460
    /* We only need to rebuild the rotation matrix if the
461
     * observation time is different from the last call */
462
261k
    double t_obs = (point.xyzt.t == HUGE_VAL) ? Q->t_epoch : point.xyzt.t;
463
261k
    if (t_obs != Q->t_obs) {
464
0
        Q->t_obs = t_obs;
465
0
        update_parameters(P);
466
0
        build_rot_matrix(P);
467
0
    }
468
469
    // Assigning in 2 steps avoids cppcheck warning
470
    // "Overlapping read/write of union is undefined behavior"
471
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
472
261k
    const auto lpz = helmert_reverse_3d(point.xyz, P);
473
261k
    point.lpz = lpz;
474
261k
}
475
476
/* Arcsecond to radians */
477
9.64k
#define ARCSEC_TO_RAD (DEG_TO_RAD / 3600.0)
478
479
52.6k
static PJ *init_helmert_six_parameters(PJ *P) {
480
52.6k
    struct pj_opaque_helmert *Q = static_cast<struct pj_opaque_helmert *>(
481
52.6k
        calloc(1, sizeof(struct pj_opaque_helmert)));
482
52.6k
    if (nullptr == Q)
483
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
484
52.6k
    P->opaque = (void *)Q;
485
486
    /* In most cases, we work on 3D cartesian coordinates */
487
52.6k
    P->left = PJ_IO_UNITS_CARTESIAN;
488
52.6k
    P->right = PJ_IO_UNITS_CARTESIAN;
489
490
    /* Translations */
491
52.6k
    if (pj_param(P->ctx, P->params, "tx").i)
492
17.2k
        Q->xyz_0.x = pj_param(P->ctx, P->params, "dx").f;
493
494
52.6k
    if (pj_param(P->ctx, P->params, "ty").i)
495
17.3k
        Q->xyz_0.y = pj_param(P->ctx, P->params, "dy").f;
496
497
52.6k
    if (pj_param(P->ctx, P->params, "tz").i)
498
17.1k
        Q->xyz_0.z = pj_param(P->ctx, P->params, "dz").f;
499
500
    /* Rotations */
501
52.6k
    if (pj_param(P->ctx, P->params, "trx").i)
502
2.45k
        Q->opk_0.o = pj_param(P->ctx, P->params, "drx").f * ARCSEC_TO_RAD;
503
504
52.6k
    if (pj_param(P->ctx, P->params, "try").i)
505
2.45k
        Q->opk_0.p = pj_param(P->ctx, P->params, "dry").f * ARCSEC_TO_RAD;
506
507
52.6k
    if (pj_param(P->ctx, P->params, "trz").i)
508
2.45k
        Q->opk_0.k = pj_param(P->ctx, P->params, "drz").f * ARCSEC_TO_RAD;
509
510
    /* Use small angle approximations? */
511
52.6k
    if (pj_param(P->ctx, P->params, "bexact").i)
512
34.6k
        Q->exact = 1;
513
514
52.6k
    return P;
515
52.6k
}
516
517
52.6k
static PJ *read_convention(PJ *P) {
518
519
52.6k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
520
521
    /* In case there are rotational terms, we require an explicit convention
522
     * to be provided. */
523
52.6k
    if (!Q->no_rotation) {
524
3.56k
        const char *convention = pj_param(P->ctx, P->params, "sconvention").s;
525
3.56k
        if (!convention) {
526
18
            proj_log_error(P, _("helmert: missing 'convention' argument"));
527
18
            return pj_default_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
528
18
        }
529
3.54k
        if (strcmp(convention, "position_vector") == 0) {
530
1.91k
            Q->is_position_vector = 1;
531
1.91k
        } else if (strcmp(convention, "coordinate_frame") == 0) {
532
1.61k
            Q->is_position_vector = 0;
533
1.61k
        } else {
534
14
            proj_log_error(
535
14
                P, _("helmert: invalid value for 'convention' argument"));
536
14
            return pj_default_destructor(P,
537
14
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
538
14
        }
539
540
        /* historically towgs84 in PROJ has always been using position_vector
541
         * convention. Accepting coordinate_frame would be confusing. */
542
3.52k
        if (pj_param_exists(P->params, "towgs84")) {
543
1.32k
            if (!Q->is_position_vector) {
544
65
                proj_log_error(P, _("helmert: towgs84 should only be used with "
545
65
                                    "convention=position_vector"));
546
65
                return pj_default_destructor(
547
65
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
548
65
            }
549
1.32k
        }
550
3.52k
    }
551
552
52.5k
    return P;
553
52.6k
}
554
555
/***********************************************************************/
556
52.5k
PJ *PJ_TRANSFORMATION(helmert, 0) {
557
    /***********************************************************************/
558
559
52.5k
    struct pj_opaque_helmert *Q;
560
561
52.5k
    if (!init_helmert_six_parameters(P)) {
562
0
        return nullptr;
563
0
    }
564
565
    /* In the 2D case, the coordinates are projected */
566
52.5k
    if (pj_param_exists(P->params, "theta")) {
567
49
        P->left = PJ_IO_UNITS_PROJECTED;
568
49
        P->right = PJ_IO_UNITS_PROJECTED;
569
49
        P->fwd = helmert_forward;
570
49
        P->inv = helmert_reverse;
571
49
    }
572
573
52.5k
    P->fwd4d = helmert_forward_4d;
574
52.5k
    P->inv4d = helmert_reverse_4d;
575
52.5k
    P->fwd3d = helmert_forward_3d;
576
52.5k
    P->inv3d = helmert_reverse_3d;
577
578
52.5k
    Q = (struct pj_opaque_helmert *)P->opaque;
579
580
    /* Detect obsolete transpose flag and error out if found */
581
52.5k
    if (pj_param(P->ctx, P->params, "ttranspose").i) {
582
0
        proj_log_error(P, _("helmert: 'transpose' argument is no longer valid. "
583
0
                            "Use convention=position_vector/coordinate_frame"));
584
0
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
585
0
    }
586
587
    /* Support the classic PROJ towgs84 parameter, but allow later overrides.*/
588
    /* Note that if towgs84 is specified, the datum_params array is set up   */
589
    /* for us automagically by the pj_datum_set call in pj_init_ctx */
590
52.5k
    if (pj_param_exists(P->params, "towgs84")) {
591
37.1k
        Q->xyz_0.x = P->datum_params[0];
592
37.1k
        Q->xyz_0.y = P->datum_params[1];
593
37.1k
        Q->xyz_0.z = P->datum_params[2];
594
595
37.1k
        Q->opk_0.o = P->datum_params[3];
596
37.1k
        Q->opk_0.p = P->datum_params[4];
597
37.1k
        Q->opk_0.k = P->datum_params[5];
598
599
        /* We must undo conversion to absolute scale from pj_datum_set */
600
37.1k
        if (0 == P->datum_params[6])
601
35.8k
            Q->scale_0 = 0;
602
1.30k
        else
603
1.30k
            Q->scale_0 = (P->datum_params[6] - 1) * 1e6;
604
37.1k
    }
605
606
52.5k
    if (pj_param(P->ctx, P->params, "ttheta").i) {
607
49
        Q->theta_0 = pj_param(P->ctx, P->params, "dtheta").f * ARCSEC_TO_RAD;
608
49
        Q->fourparam = 1;
609
49
        Q->scale_0 = 1.0; /* default scale for the 4-param shift */
610
49
    }
611
612
    /* Scale */
613
52.5k
    if (pj_param(P->ctx, P->params, "ts").i) {
614
2.47k
        Q->scale_0 = pj_param(P->ctx, P->params, "ds").f;
615
2.47k
        if (Q->scale_0 <= -1.0e6) {
616
1
            proj_log_error(P, _("helmert: invalid value for s."));
617
1
            return pj_default_destructor(P,
618
1
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
619
1
        }
620
2.47k
        if (pj_param(P->ctx, P->params, "ttheta").i && Q->scale_0 == 0.0) {
621
1
            proj_log_error(P, _("helmert: invalid value for s."));
622
1
            return pj_default_destructor(P,
623
1
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
624
1
        }
625
2.47k
    }
626
627
    /* Translation rates */
628
52.5k
    if (pj_param(P->ctx, P->params, "tdx").i)
629
735
        Q->dxyz.x = pj_param(P->ctx, P->params, "ddx").f;
630
631
52.5k
    if (pj_param(P->ctx, P->params, "tdy").i)
632
735
        Q->dxyz.y = pj_param(P->ctx, P->params, "ddy").f;
633
634
52.5k
    if (pj_param(P->ctx, P->params, "tdz").i)
635
735
        Q->dxyz.z = pj_param(P->ctx, P->params, "ddz").f;
636
637
    /* Rotations rates */
638
52.5k
    if (pj_param(P->ctx, P->params, "tdrx").i)
639
735
        Q->dopk.o = pj_param(P->ctx, P->params, "ddrx").f * ARCSEC_TO_RAD;
640
641
52.5k
    if (pj_param(P->ctx, P->params, "tdry").i)
642
738
        Q->dopk.p = pj_param(P->ctx, P->params, "ddry").f * ARCSEC_TO_RAD;
643
644
52.5k
    if (pj_param(P->ctx, P->params, "tdrz").i)
645
737
        Q->dopk.k = pj_param(P->ctx, P->params, "ddrz").f * ARCSEC_TO_RAD;
646
647
52.5k
    if (pj_param(P->ctx, P->params, "tdtheta").i)
648
18
        Q->dtheta = pj_param(P->ctx, P->params, "ddtheta").f * ARCSEC_TO_RAD;
649
650
    /* Scale rate */
651
52.5k
    if (pj_param(P->ctx, P->params, "tds").i)
652
735
        Q->dscale = pj_param(P->ctx, P->params, "dds").f;
653
654
    /* Epoch */
655
52.5k
    if (pj_param(P->ctx, P->params, "tt_epoch").i)
656
741
        Q->t_epoch = pj_param(P->ctx, P->params, "dt_epoch").f;
657
658
52.5k
    Q->xyz = Q->xyz_0;
659
52.5k
    Q->opk = Q->opk_0;
660
52.5k
    Q->scale = Q->scale_0;
661
52.5k
    Q->theta = Q->theta_0;
662
663
52.5k
    if ((Q->opk.o == 0) && (Q->opk.p == 0) && (Q->opk.k == 0) &&
664
52.5k
        (Q->dopk.o == 0) && (Q->dopk.p == 0) && (Q->dopk.k == 0)) {
665
49.0k
        Q->no_rotation = 1;
666
49.0k
    }
667
668
52.5k
    if (!read_convention(P)) {
669
85
        return nullptr;
670
85
    }
671
672
    /* Let's help with debugging */
673
52.5k
    if (proj_log_level(P->ctx, PJ_LOG_TELL) >= PJ_LOG_TRACE) {
674
0
        proj_log_trace(P, "Helmert parameters:");
675
0
        proj_log_trace(P, "x=  %8.5f  y=  %8.5f  z=  %8.5f", Q->xyz.x, Q->xyz.y,
676
0
                       Q->xyz.z);
677
0
        proj_log_trace(P, "rx= %8.5f  ry= %8.5f  rz= %8.5f",
678
0
                       Q->opk.o / ARCSEC_TO_RAD, Q->opk.p / ARCSEC_TO_RAD,
679
0
                       Q->opk.k / ARCSEC_TO_RAD);
680
0
        proj_log_trace(P, "s=  %8.5f  exact=%d%s", Q->scale, Q->exact,
681
0
                       Q->no_rotation ? ""
682
0
                       : Q->is_position_vector
683
0
                           ? "  convention=position_vector"
684
0
                           : "  convention=coordinate_frame");
685
0
        proj_log_trace(P, "dx= %8.5f  dy= %8.5f  dz= %8.5f", Q->dxyz.x,
686
0
                       Q->dxyz.y, Q->dxyz.z);
687
0
        proj_log_trace(P, "drx=%8.5f  dry=%8.5f  drz=%8.5f", Q->dopk.o,
688
0
                       Q->dopk.p, Q->dopk.k);
689
0
        proj_log_trace(P, "ds= %8.5f  t_epoch=%8.5f", Q->dscale, Q->t_epoch);
690
0
    }
691
692
52.5k
    update_parameters(P);
693
52.5k
    build_rot_matrix(P);
694
695
52.5k
    return P;
696
52.5k
}
697
698
/***********************************************************************/
699
40
PJ *PJ_TRANSFORMATION(molobadekas, 0) {
700
    /***********************************************************************/
701
702
40
    struct pj_opaque_helmert *Q;
703
704
40
    if (!init_helmert_six_parameters(P)) {
705
0
        return nullptr;
706
0
    }
707
708
40
    P->fwd3d = helmert_forward_3d;
709
40
    P->inv3d = helmert_reverse_3d;
710
711
40
    Q = (struct pj_opaque_helmert *)P->opaque;
712
713
    /* Scale */
714
40
    if (pj_param(P->ctx, P->params, "ts").i) {
715
13
        Q->scale_0 = pj_param(P->ctx, P->params, "ds").f;
716
13
    }
717
718
40
    Q->opk = Q->opk_0;
719
40
    Q->scale = Q->scale_0;
720
721
40
    if (!read_convention(P)) {
722
12
        return nullptr;
723
12
    }
724
725
    /* Reference point */
726
28
    if (pj_param(P->ctx, P->params, "tpx").i)
727
10
        Q->refp.x = pj_param(P->ctx, P->params, "dpx").f;
728
729
28
    if (pj_param(P->ctx, P->params, "tpy").i)
730
13
        Q->refp.y = pj_param(P->ctx, P->params, "dpy").f;
731
732
28
    if (pj_param(P->ctx, P->params, "tpz").i)
733
10
        Q->refp.z = pj_param(P->ctx, P->params, "dpz").f;
734
735
    /* Let's help with debugging */
736
28
    if (proj_log_level(P->ctx, PJ_LOG_TELL) >= PJ_LOG_TRACE) {
737
0
        proj_log_trace(P, "Molodensky-Badekas parameters:");
738
0
        proj_log_trace(P, "x=  %8.5f  y=  %8.5f  z=  %8.5f", Q->xyz_0.x,
739
0
                       Q->xyz_0.y, Q->xyz_0.z);
740
0
        proj_log_trace(P, "rx= %8.5f  ry= %8.5f  rz= %8.5f",
741
0
                       Q->opk.o / ARCSEC_TO_RAD, Q->opk.p / ARCSEC_TO_RAD,
742
0
                       Q->opk.k / ARCSEC_TO_RAD);
743
0
        proj_log_trace(P, "s=  %8.5f  exact=%d%s", Q->scale, Q->exact,
744
0
                       Q->is_position_vector ? "  convention=position_vector"
745
0
                                             : "  convention=coordinate_frame");
746
0
        proj_log_trace(P, "px= %8.5f  py= %8.5f  pz= %8.5f", Q->refp.x,
747
0
                       Q->refp.y, Q->refp.z);
748
0
    }
749
750
    /* as an optimization, we incorporate the refp in the translation terms */
751
28
    Q->xyz_0.x += Q->refp.x;
752
28
    Q->xyz_0.y += Q->refp.y;
753
28
    Q->xyz_0.z += Q->refp.z;
754
755
28
    Q->xyz = Q->xyz_0;
756
757
28
    build_rot_matrix(P);
758
759
28
    return P;
760
40
}