Coverage Report

Created: 2025-11-16 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/proj/src/transformations/helmert.cpp
Line
Count
Source
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
5.17k
#define R00 (Q->R[0][0])
90
7.54k
#define R01 (Q->R[0][1])
91
7.54k
#define R02 (Q->R[0][2])
92
93
7.54k
#define R10 (Q->R[1][0])
94
5.17k
#define R11 (Q->R[1][1])
95
7.54k
#define R12 (Q->R[1][2])
96
97
7.54k
#define R20 (Q->R[2][0])
98
7.54k
#define R21 (Q->R[2][1])
99
5.17k
#define R22 (Q->R[2][2])
100
101
/**************************************************************************/
102
5.10k
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
5.10k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
129
5.10k
    double dt = Q->t_obs - Q->t_epoch;
130
131
5.10k
    Q->xyz.x = Q->xyz_0.x + Q->dxyz.x * dt;
132
5.10k
    Q->xyz.y = Q->xyz_0.y + Q->dxyz.y * dt;
133
5.10k
    Q->xyz.z = Q->xyz_0.z + Q->dxyz.z * dt;
134
135
5.10k
    Q->opk.o = Q->opk_0.o + Q->dopk.o * dt;
136
5.10k
    Q->opk.p = Q->opk_0.p + Q->dopk.p * dt;
137
5.10k
    Q->opk.k = Q->opk_0.k + Q->dopk.k * dt;
138
139
5.10k
    Q->scale = Q->scale_0 + Q->dscale * dt;
140
141
5.10k
    Q->theta = Q->theta_0 + Q->dtheta * dt;
142
143
    /* debugging output */
144
5.10k
    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
5.10k
}
159
160
/**************************************************************************/
161
5.17k
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
5.17k
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
229
230
5.17k
    double f, t, p;    /* phi/fi , theta, psi  */
231
5.17k
    double cf, ct, cp; /* cos (fi, theta, psi) */
232
5.17k
    double sf, st, sp; /* sin (fi, theta, psi) */
233
234
    /* rename   (omega, phi, kappa)   to   (fi, theta, psi)   */
235
5.17k
    f = Q->opk.o;
236
5.17k
    t = Q->opk.p;
237
5.17k
    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
5.17k
    if (Q->exact) {
243
3.41k
        cf = cos(f);
244
3.41k
        sf = sin(f);
245
3.41k
        ct = cos(t);
246
3.41k
        st = sin(t);
247
3.41k
        cp = cos(p);
248
3.41k
        sp = sin(p);
249
250
3.41k
        R00 = ct * cp;
251
3.41k
        R01 = cf * sp + sf * st * cp;
252
3.41k
        R02 = sf * sp - cf * st * cp;
253
254
3.41k
        R10 = -ct * sp;
255
3.41k
        R11 = cf * cp - sf * st * sp;
256
3.41k
        R12 = sf * cp + cf * st * sp;
257
258
3.41k
        R20 = st;
259
3.41k
        R21 = -sf * ct;
260
3.41k
        R22 = cf * ct;
261
3.41k
    } else {
262
1.76k
        R00 = 1;
263
1.76k
        R01 = p;
264
1.76k
        R02 = -t;
265
266
1.76k
        R10 = -p;
267
1.76k
        R11 = 1;
268
1.76k
        R12 = f;
269
270
1.76k
        R20 = t;
271
1.76k
        R21 = -f;
272
1.76k
        R22 = 1;
273
1.76k
    }
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
5.17k
    if (Q->is_position_vector) {
302
1.18k
        double r;
303
1.18k
        r = R01;
304
1.18k
        R01 = R10;
305
1.18k
        R10 = r;
306
1.18k
        r = R02;
307
1.18k
        R02 = R20;
308
1.18k
        R20 = r;
309
1.18k
        r = R12;
310
1.18k
        R12 = R21;
311
1.18k
        R21 = r;
312
1.18k
    }
313
314
    /* some debugging output */
315
5.17k
    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
5.17k
}
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
0
static PJ_XYZ helmert_forward_3d(PJ_LPZ lpz, PJ *P) {
363
    /***********************************************************************/
364
0
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
365
0
    PJ_COORD point = {{0, 0, 0, 0}};
366
0
    double X, Y, Z, scale;
367
368
0
    point.lpz = lpz;
369
370
0
    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
0
    if (Q->no_rotation && Q->scale == 0) {
377
0
        point.xyz.x = lpz.lam + Q->xyz.x;
378
0
        point.xyz.y = lpz.phi + Q->xyz.y;
379
0
        point.xyz.z = lpz.z + Q->xyz.z;
380
0
        return point.xyz;
381
0
    }
382
383
0
    scale = 1 + Q->scale * 1e-6;
384
385
0
    X = lpz.lam - Q->refp.x;
386
0
    Y = lpz.phi - Q->refp.y;
387
0
    Z = lpz.z - Q->refp.z;
388
389
0
    point.xyz.x = scale * (R00 * X + R01 * Y + R02 * Z);
390
0
    point.xyz.y = scale * (R10 * X + R11 * Y + R12 * Z);
391
0
    point.xyz.z = scale * (R20 * X + R21 * Y + R22 * Z);
392
393
0
    point.xyz.x += Q->xyz.x; /* for Molodensky-Badekas, Q->xyz already
394
                                incorporates the Q->refp offset */
395
0
    point.xyz.y += Q->xyz.y;
396
0
    point.xyz.z += Q->xyz.z;
397
398
0
    return point.xyz;
399
0
}
400
401
/***********************************************************************/
402
0
static PJ_LPZ helmert_reverse_3d(PJ_XYZ xyz, PJ *P) {
403
    /***********************************************************************/
404
0
    struct pj_opaque_helmert *Q = (struct pj_opaque_helmert *)P->opaque;
405
0
    PJ_COORD point = {{0, 0, 0, 0}};
406
0
    double X, Y, Z, scale;
407
408
0
    point.xyz = xyz;
409
410
0
    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
0
    if (Q->no_rotation && Q->scale == 0) {
417
0
        point.xyz.x = xyz.x - Q->xyz.x;
418
0
        point.xyz.y = xyz.y - Q->xyz.y;
419
0
        point.xyz.z = xyz.z - Q->xyz.z;
420
0
        return point.lpz;
421
0
    }
422
423
0
    scale = 1 + Q->scale * 1e-6;
424
425
    /* Unscale and deoffset */
426
0
    X = (xyz.x - Q->xyz.x) / scale;
427
0
    Y = (xyz.y - Q->xyz.y) / scale;
428
0
    Z = (xyz.z - Q->xyz.z) / scale;
429
430
    /* Inverse rotation through transpose multiplication */
431
0
    point.xyz.x = (R00 * X + R10 * Y + R20 * Z) + Q->refp.x;
432
0
    point.xyz.y = (R01 * X + R11 * Y + R21 * Z) + Q->refp.y;
433
0
    point.xyz.z = (R02 * X + R12 * Y + R22 * Z) + Q->refp.z;
434
435
0
    return point.lpz;
436
0
}
437
438
0
static void helmert_forward_4d(PJ_COORD &point, PJ *P) {
439
0
    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
0
    double t_obs = (point.xyzt.t == HUGE_VAL) ? Q->t_epoch : point.xyzt.t;
444
0
    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
0
    const auto xyz = helmert_forward_3d(point.lpz, P);
454
0
    point.xyz = xyz;
455
0
}
456
457
0
static void helmert_reverse_4d(PJ_COORD &point, PJ *P) {
458
0
    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
0
    double t_obs = (point.xyzt.t == HUGE_VAL) ? Q->t_epoch : point.xyzt.t;
463
0
    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
0
    const auto lpz = helmert_reverse_3d(point.xyz, P);
473
0
    point.lpz = lpz;
474
0
}
475
476
/* Arcsecond to radians */
477
945
#define ARCSEC_TO_RAD (DEG_TO_RAD / 3600.0)
478
479
5.18k
static PJ *init_helmert_six_parameters(PJ *P) {
480
5.18k
    struct pj_opaque_helmert *Q = static_cast<struct pj_opaque_helmert *>(
481
5.18k
        calloc(1, sizeof(struct pj_opaque_helmert)));
482
5.18k
    if (nullptr == Q)
483
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
484
5.18k
    P->opaque = (void *)Q;
485
486
    /* In most cases, we work on 3D cartesian coordinates */
487
5.18k
    P->left = PJ_IO_UNITS_CARTESIAN;
488
5.18k
    P->right = PJ_IO_UNITS_CARTESIAN;
489
490
    /* Translations */
491
5.18k
    if (pj_param(P->ctx, P->params, "tx").i)
492
801
        Q->xyz_0.x = pj_param(P->ctx, P->params, "dx").f;
493
494
5.18k
    if (pj_param(P->ctx, P->params, "ty").i)
495
393
        Q->xyz_0.y = pj_param(P->ctx, P->params, "dy").f;
496
497
5.18k
    if (pj_param(P->ctx, P->params, "tz").i)
498
548
        Q->xyz_0.z = pj_param(P->ctx, P->params, "dz").f;
499
500
    /* Rotations */
501
5.18k
    if (pj_param(P->ctx, P->params, "trx").i)
502
171
        Q->opk_0.o = pj_param(P->ctx, P->params, "drx").f * ARCSEC_TO_RAD;
503
504
5.18k
    if (pj_param(P->ctx, P->params, "try").i)
505
171
        Q->opk_0.p = pj_param(P->ctx, P->params, "dry").f * ARCSEC_TO_RAD;
506
507
5.18k
    if (pj_param(P->ctx, P->params, "trz").i)
508
171
        Q->opk_0.k = pj_param(P->ctx, P->params, "drz").f * ARCSEC_TO_RAD;
509
510
    /* Use small angle approximations? */
511
5.18k
    if (pj_param(P->ctx, P->params, "bexact").i)
512
3.41k
        Q->exact = 1;
513
514
5.18k
    return P;
515
5.18k
}
516
517
5.18k
static PJ *read_convention(PJ *P) {
518
519
5.18k
    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
5.18k
    if (!Q->no_rotation) {
524
1.21k
        const char *convention = pj_param(P->ctx, P->params, "sconvention").s;
525
1.21k
        if (!convention) {
526
4
            proj_log_error(P, _("helmert: missing 'convention' argument"));
527
4
            return pj_default_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
528
4
        }
529
1.21k
        if (strcmp(convention, "position_vector") == 0) {
530
1.18k
            Q->is_position_vector = 1;
531
1.18k
        } else if (strcmp(convention, "coordinate_frame") == 0) {
532
27
            Q->is_position_vector = 0;
533
27
        } else {
534
3
            proj_log_error(
535
3
                P, _("helmert: invalid value for 'convention' argument"));
536
3
            return pj_default_destructor(P,
537
3
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
538
3
        }
539
540
        /* historically towgs84 in PROJ has always been using position_vector
541
         * convention. Accepting coordinate_frame would be confusing. */
542
1.21k
        if (pj_param_exists(P->params, "towgs84")) {
543
1.14k
            if (!Q->is_position_vector) {
544
0
                proj_log_error(P, _("helmert: towgs84 should only be used with "
545
0
                                    "convention=position_vector"));
546
0
                return pj_default_destructor(
547
0
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
548
0
            }
549
1.14k
        }
550
1.21k
    }
551
552
5.17k
    return P;
553
5.18k
}
554
555
/***********************************************************************/
556
5.11k
PJ *PJ_TRANSFORMATION(helmert, 0) {
557
    /***********************************************************************/
558
559
5.11k
    struct pj_opaque_helmert *Q;
560
561
5.11k
    if (!init_helmert_six_parameters(P)) {
562
0
        return nullptr;
563
0
    }
564
565
    /* In the 2D case, the coordinates are projected */
566
5.11k
    if (pj_param_exists(P->params, "theta")) {
567
0
        P->left = PJ_IO_UNITS_PROJECTED;
568
0
        P->right = PJ_IO_UNITS_PROJECTED;
569
0
        P->fwd = helmert_forward;
570
0
        P->inv = helmert_reverse;
571
0
    }
572
573
5.11k
    P->fwd4d = helmert_forward_4d;
574
5.11k
    P->inv4d = helmert_reverse_4d;
575
5.11k
    P->fwd3d = helmert_forward_3d;
576
5.11k
    P->inv3d = helmert_reverse_3d;
577
578
5.11k
    Q = (struct pj_opaque_helmert *)P->opaque;
579
580
    /* Detect obsolete transpose flag and error out if found */
581
5.11k
    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
5.11k
    if (pj_param_exists(P->params, "towgs84")) {
591
3.41k
        Q->xyz_0.x = P->datum_params[0];
592
3.41k
        Q->xyz_0.y = P->datum_params[1];
593
3.41k
        Q->xyz_0.z = P->datum_params[2];
594
595
3.41k
        Q->opk_0.o = P->datum_params[3];
596
3.41k
        Q->opk_0.p = P->datum_params[4];
597
3.41k
        Q->opk_0.k = P->datum_params[5];
598
599
        /* We must undo conversion to absolute scale from pj_datum_set */
600
3.41k
        if (0 == P->datum_params[6])
601
2.22k
            Q->scale_0 = 0;
602
1.19k
        else
603
1.19k
            Q->scale_0 = (P->datum_params[6] - 1) * 1e6;
604
3.41k
    }
605
606
5.11k
    if (pj_param(P->ctx, P->params, "ttheta").i) {
607
0
        Q->theta_0 = pj_param(P->ctx, P->params, "dtheta").f * ARCSEC_TO_RAD;
608
0
        Q->fourparam = 1;
609
0
        Q->scale_0 = 1.0; /* default scale for the 4-param shift */
610
0
    }
611
612
    /* Scale */
613
5.11k
    if (pj_param(P->ctx, P->params, "ts").i) {
614
334
        Q->scale_0 = pj_param(P->ctx, P->params, "ds").f;
615
334
        if (Q->scale_0 <= -1.0e6) {
616
0
            proj_log_error(P, _("helmert: invalid value for s."));
617
0
            return pj_default_destructor(P,
618
0
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
619
0
        }
620
334
        if (pj_param(P->ctx, P->params, "ttheta").i && Q->scale_0 == 0.0) {
621
0
            proj_log_error(P, _("helmert: invalid value for s."));
622
0
            return pj_default_destructor(P,
623
0
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
624
0
        }
625
334
    }
626
627
    /* Translation rates */
628
5.11k
    if (pj_param(P->ctx, P->params, "tdx").i)
629
81
        Q->dxyz.x = pj_param(P->ctx, P->params, "ddx").f;
630
631
5.11k
    if (pj_param(P->ctx, P->params, "tdy").i)
632
81
        Q->dxyz.y = pj_param(P->ctx, P->params, "ddy").f;
633
634
5.11k
    if (pj_param(P->ctx, P->params, "tdz").i)
635
81
        Q->dxyz.z = pj_param(P->ctx, P->params, "ddz").f;
636
637
    /* Rotations rates */
638
5.11k
    if (pj_param(P->ctx, P->params, "tdrx").i)
639
81
        Q->dopk.o = pj_param(P->ctx, P->params, "ddrx").f * ARCSEC_TO_RAD;
640
641
5.11k
    if (pj_param(P->ctx, P->params, "tdry").i)
642
262
        Q->dopk.p = pj_param(P->ctx, P->params, "ddry").f * ARCSEC_TO_RAD;
643
644
5.11k
    if (pj_param(P->ctx, P->params, "tdrz").i)
645
89
        Q->dopk.k = pj_param(P->ctx, P->params, "ddrz").f * ARCSEC_TO_RAD;
646
647
5.11k
    if (pj_param(P->ctx, P->params, "tdtheta").i)
648
0
        Q->dtheta = pj_param(P->ctx, P->params, "ddtheta").f * ARCSEC_TO_RAD;
649
650
    /* Scale rate */
651
5.11k
    if (pj_param(P->ctx, P->params, "tds").i)
652
198
        Q->dscale = pj_param(P->ctx, P->params, "dds").f;
653
654
    /* Epoch */
655
5.11k
    if (pj_param(P->ctx, P->params, "tt_epoch").i)
656
119
        Q->t_epoch = pj_param(P->ctx, P->params, "dt_epoch").f;
657
658
5.11k
    Q->xyz = Q->xyz_0;
659
5.11k
    Q->opk = Q->opk_0;
660
5.11k
    Q->scale = Q->scale_0;
661
5.11k
    Q->theta = Q->theta_0;
662
663
5.11k
    if ((Q->opk.o == 0) && (Q->opk.p == 0) && (Q->opk.k == 0) &&
664
3.99k
        (Q->dopk.o == 0) && (Q->dopk.p == 0) && (Q->dopk.k == 0)) {
665
3.96k
        Q->no_rotation = 1;
666
3.96k
    }
667
668
5.11k
    if (!read_convention(P)) {
669
5
        return nullptr;
670
5
    }
671
672
    /* Let's help with debugging */
673
5.10k
    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
5.10k
    update_parameters(P);
693
5.10k
    build_rot_matrix(P);
694
695
5.10k
    return P;
696
5.11k
}
697
698
/***********************************************************************/
699
73
PJ *PJ_TRANSFORMATION(molobadekas, 0) {
700
    /***********************************************************************/
701
702
73
    struct pj_opaque_helmert *Q;
703
704
73
    if (!init_helmert_six_parameters(P)) {
705
0
        return nullptr;
706
0
    }
707
708
73
    P->fwd3d = helmert_forward_3d;
709
73
    P->inv3d = helmert_reverse_3d;
710
711
73
    Q = (struct pj_opaque_helmert *)P->opaque;
712
713
    /* Scale */
714
73
    if (pj_param(P->ctx, P->params, "ts").i) {
715
71
        Q->scale_0 = pj_param(P->ctx, P->params, "ds").f;
716
71
    }
717
718
73
    Q->opk = Q->opk_0;
719
73
    Q->scale = Q->scale_0;
720
721
73
    if (!read_convention(P)) {
722
2
        return nullptr;
723
2
    }
724
725
    /* Reference point */
726
71
    if (pj_param(P->ctx, P->params, "tpx").i)
727
71
        Q->refp.x = pj_param(P->ctx, P->params, "dpx").f;
728
729
71
    if (pj_param(P->ctx, P->params, "tpy").i)
730
71
        Q->refp.y = pj_param(P->ctx, P->params, "dpy").f;
731
732
71
    if (pj_param(P->ctx, P->params, "tpz").i)
733
71
        Q->refp.z = pj_param(P->ctx, P->params, "dpz").f;
734
735
    /* Let's help with debugging */
736
71
    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
71
    Q->xyz_0.x += Q->refp.x;
752
71
    Q->xyz_0.y += Q->refp.y;
753
71
    Q->xyz_0.z += Q->refp.z;
754
755
71
    Q->xyz = Q->xyz_0;
756
757
71
    build_rot_matrix(P);
758
759
71
    return P;
760
73
}