Coverage Report

Created: 2025-07-23 06:58

/src/PROJ/src/transformations/deformation.cpp
Line
Count
Source (jump to first uncovered line)
1
/***********************************************************************
2
3
        Kinematic datum shifting utilizing a deformation model
4
5
                    Kristian Evers, 2017-10-29
6
7
************************************************************************
8
9
Perform datum shifts by means of a deformation/velocity model.
10
11
    X_out = X_in + (T_obs - T_epoch) * DX
12
    Y_out = Y_in + (T_obs - T_epoch) * DY
13
    Z_out = Z_in + (T_obs - T_epoch) * DZ
14
15
The deformation operation takes cartesian coordinates as input and
16
returns cartesian coordinates as well.
17
18
Corrections in the gridded model are in east, north, up (ENU) space.
19
Hence the input coordinates need to be converted to ENU-space when
20
searching for corrections in the grid. The corrections are then converted
21
to cartesian PJ_XYZ-space and applied to the input coordinates (also in
22
cartesian space).
23
24
A full deformation model is preferably represented as a 3 channel Geodetic
25
TIFF Grid, but was historically described by a set of two grids: One for
26
the horizontal components and one for the vertical component.
27
28
The east and north components are (were) stored using the CTable/CTable2
29
format, up component is (was) stored in the GTX format. Both grids are
30
(were) expected to contain grid-values in units of mm/year in ENU-space.
31
32
************************************************************************
33
* Copyright (c) 2017, Kristian Evers
34
*
35
* Permission is hereby granted, free of charge, to any person obtaining a
36
* copy of this software and associated documentation files (the "Software"),
37
* to deal in the Software without restriction, including without limitation
38
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
39
* and/or sell copies of the Software, and to permit persons to whom the
40
* Software is furnished to do so, subject to the following conditions:
41
*
42
* The above copyright notice and this permission notice shall be included
43
* in all copies or substantial portions of the Software.
44
*
45
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
46
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
48
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
50
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
51
* DEALINGS IN THE SOFTWARE.
52
*
53
***********************************************************************/
54
55
#include "grids.hpp"
56
#include "proj.h"
57
#include "proj_internal.h"
58
#include <errno.h>
59
#include <math.h>
60
61
#include <algorithm>
62
63
PROJ_HEAD(deformation, "Kinematic grid shift");
64
65
0
#define TOL 1e-8
66
0
#define MAX_ITERATIONS 10
67
68
using namespace NS_PROJ;
69
70
namespace { // anonymous namespace
71
struct deformationData {
72
    double dt = 0;
73
    double t_epoch = 0;
74
    PJ *cart = nullptr;
75
    ListOfGenericGrids grids{};
76
    ListOfHGrids hgrids{};
77
    ListOfVGrids vgrids{};
78
};
79
} // anonymous namespace
80
81
// ---------------------------------------------------------------------------
82
83
static bool pj_deformation_get_grid_values(PJ *P, deformationData *Q,
84
                                           const PJ_LP &lp, double &vx,
85
588
                                           double &vy, double &vz) {
86
588
    GenericShiftGridSet *gridset = nullptr;
87
588
    auto grid = pj_find_generic_grid(Q->grids, lp, gridset);
88
588
    if (!grid) {
89
0
        return false;
90
0
    }
91
588
    if (grid->isNullGrid()) {
92
588
        vx = 0;
93
588
        vy = 0;
94
588
        vz = 0;
95
588
        return true;
96
588
    }
97
0
    const auto samplesPerPixel = grid->samplesPerPixel();
98
0
    if (samplesPerPixel < 3) {
99
0
        proj_log_error(P, "grid has not enough samples");
100
0
        return false;
101
0
    }
102
0
    int sampleE = 0;
103
0
    int sampleN = 1;
104
0
    int sampleU = 2;
105
0
    for (int i = 0; i < samplesPerPixel; i++) {
106
0
        const auto desc = grid->description(i);
107
0
        if (desc == "east_velocity") {
108
0
            sampleE = i;
109
0
        } else if (desc == "north_velocity") {
110
0
            sampleN = i;
111
0
        } else if (desc == "up_velocity") {
112
0
            sampleU = i;
113
0
        }
114
0
    }
115
0
    const auto unit = grid->unit(sampleE);
116
0
    if (!unit.empty() && unit != "millimetres per year") {
117
0
        proj_log_error(P, "Only unit=millimetres per year currently handled");
118
0
        return false;
119
0
    }
120
121
0
    bool must_retry = false;
122
0
    if (!pj_bilinear_interpolation_three_samples(P->ctx, grid, lp, sampleE,
123
0
                                                 sampleN, sampleU, vx, vy, vz,
124
0
                                                 must_retry)) {
125
0
        if (must_retry)
126
0
            return pj_deformation_get_grid_values(P, Q, lp, vx, vy, vz);
127
0
        return false;
128
0
    }
129
    // divide by 1000 to get m/year
130
0
    vx /= 1000;
131
0
    vy /= 1000;
132
0
    vz /= 1000;
133
0
    return true;
134
0
}
135
136
/********************************************************************************/
137
588
static PJ_XYZ pj_deformation_get_grid_shift(PJ *P, const PJ_XYZ &cartesian) {
138
    /********************************************************************************
139
        Read correction values from grid. The cartesian input coordinates are
140
        converted to geodetic coordinates in order look up the correction values
141
        in the grid. Once the grid corrections are read we need to convert them
142
        from ENU-space to cartesian PJ_XYZ-space. ENU -> PJ_XYZ formula
143
    described in:
144
145
        Nørbech, T., et al, 2003(?), "Transformation from a Common Nordic
146
    Reference Frame to ETRS89 in Denmark, Finland, Norway, and Sweden – status
147
    report"
148
149
    ********************************************************************************/
150
588
    PJ_COORD geodetic, shift, temp;
151
588
    double sp, cp, sl, cl;
152
588
    int previous_errno = proj_errno_reset(P);
153
588
    auto Q = static_cast<deformationData *>(P->opaque);
154
155
    /* cartesian to geodetic */
156
588
    geodetic.lpz = pj_inv3d(cartesian, Q->cart);
157
158
    /* look up correction values in grids */
159
588
    if (!Q->grids.empty()) {
160
588
        double vx = 0;
161
588
        double vy = 0;
162
588
        double vz = 0;
163
588
        if (!pj_deformation_get_grid_values(P, Q, geodetic.lp, vx, vy, vz)) {
164
0
            return proj_coord_error().xyz;
165
0
        }
166
588
        shift.xyz.x = vx;
167
588
        shift.xyz.y = vy;
168
588
        shift.xyz.z = vz;
169
588
    } else {
170
0
        shift.lp = pj_hgrid_value(P, Q->hgrids, geodetic.lp);
171
0
        shift.enu.u = pj_vgrid_value(P, Q->vgrids, geodetic.lp, 1.0);
172
173
0
        if (proj_errno(P) == PROJ_ERR_COORD_TRANSFM_OUTSIDE_GRID)
174
0
            proj_log_debug(
175
0
                P, "coordinate (%.3f, %.3f) outside deformation model",
176
0
                proj_todeg(geodetic.lpz.lam), proj_todeg(geodetic.lpz.phi));
177
178
        /* grid values are stored as mm/yr, we need m/yr */
179
0
        shift.xyz.x /= 1000;
180
0
        shift.xyz.y /= 1000;
181
0
        shift.xyz.z /= 1000;
182
0
    }
183
184
    /* pre-calc cosines and sines */
185
588
    sp = sin(geodetic.lpz.phi);
186
588
    cp = cos(geodetic.lpz.phi);
187
588
    sl = sin(geodetic.lpz.lam);
188
588
    cl = cos(geodetic.lpz.lam);
189
190
    /* ENU -> PJ_XYZ */
191
588
    temp.xyz.x =
192
588
        -sp * cl * shift.enu.n - sl * shift.enu.e + cp * cl * shift.enu.u;
193
588
    temp.xyz.y =
194
588
        -sp * sl * shift.enu.n + cl * shift.enu.e + cp * sl * shift.enu.u;
195
588
    temp.xyz.z = cp * shift.enu.n + sp * shift.enu.u;
196
197
588
    shift.xyz = temp.xyz;
198
199
588
    proj_errno_restore(P, previous_errno);
200
201
588
    return shift.xyz;
202
588
}
203
204
/********************************************************************************/
205
static PJ_XYZ pj_deformation_reverse_shift(PJ *P, const PJ_XYZ &input,
206
0
                                           double dt) {
207
    /********************************************************************************
208
        Iteratively determine the reverse grid shift correction values.
209
    *********************************************************************************/
210
0
    PJ_XYZ out, delta, dif;
211
0
    double z0;
212
0
    int i = MAX_ITERATIONS;
213
214
0
    delta = pj_deformation_get_grid_shift(P, input);
215
0
    if (delta.x == HUGE_VAL) {
216
0
        return delta;
217
0
    }
218
219
    /* Store the original z shift for later application */
220
0
    z0 = delta.z;
221
222
    /* When iterating to find the best horizontal coordinate we also carry   */
223
    /* along the z-component, since we need it for the cartesian -> geodetic */
224
    /* conversion. The z-component adjustment is overwritten with z0 after   */
225
    /* the loop has finished.                                                */
226
0
    out.x = input.x - dt * delta.x;
227
0
    out.y = input.y - dt * delta.y;
228
0
    out.z = input.z + dt * delta.z;
229
230
0
    do {
231
0
        delta = pj_deformation_get_grid_shift(P, out);
232
233
0
        if (delta.x == HUGE_VAL)
234
0
            break;
235
236
0
        dif.x = out.x + dt * delta.x - input.x;
237
0
        dif.y = out.y + dt * delta.y - input.y;
238
0
        dif.z = out.z - dt * delta.z - input.z;
239
0
        out.x += dif.x;
240
0
        out.y += dif.y;
241
0
        out.z += dif.z;
242
243
0
    } while (--i && hypot(dif.x, dif.y) > TOL);
244
245
0
    out.z = input.z - dt * z0;
246
247
0
    return out;
248
0
}
249
250
0
static PJ_XYZ pj_deformation_forward_3d(PJ_LPZ lpz, PJ *P) {
251
0
    struct deformationData *Q = (struct deformationData *)P->opaque;
252
0
    PJ_COORD out, in;
253
0
    PJ_XYZ shift;
254
0
    in.lpz = lpz;
255
0
    out = in;
256
257
0
    if (Q->dt == HUGE_VAL) {
258
0
        out = proj_coord_error(); /* in the 3D case +t_obs must be specified */
259
0
        proj_log_debug(P, "+dt must be specified");
260
0
        return out.xyz;
261
0
    }
262
263
0
    shift = pj_deformation_get_grid_shift(P, in.xyz);
264
0
    if (shift.x == HUGE_VAL) {
265
0
        return shift;
266
0
    }
267
268
0
    out.xyz.x += Q->dt * shift.x;
269
0
    out.xyz.y += Q->dt * shift.y;
270
0
    out.xyz.z += Q->dt * shift.z;
271
272
0
    return out.xyz;
273
0
}
274
275
588
static void pj_deformation_forward_4d(PJ_COORD &coo, PJ *P) {
276
588
    struct deformationData *Q = (struct deformationData *)P->opaque;
277
588
    double dt;
278
588
    PJ_XYZ shift;
279
280
588
    if (Q->dt != HUGE_VAL) {
281
588
        dt = Q->dt;
282
588
    } else {
283
0
        if (coo.xyzt.t == HUGE_VAL) {
284
0
            coo = proj_coord_error();
285
0
            proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_MISSING_TIME);
286
0
            return;
287
0
        }
288
0
        dt = coo.xyzt.t - Q->t_epoch;
289
0
    }
290
291
588
    shift = pj_deformation_get_grid_shift(P, coo.xyz);
292
293
588
    coo.xyzt.x += dt * shift.x;
294
588
    coo.xyzt.y += dt * shift.y;
295
588
    coo.xyzt.z += dt * shift.z;
296
588
}
297
298
0
static PJ_LPZ pj_deformation_reverse_3d(PJ_XYZ in, PJ *P) {
299
0
    struct deformationData *Q = (struct deformationData *)P->opaque;
300
0
    PJ_COORD out;
301
0
    out.xyz = in;
302
303
0
    if (Q->dt == HUGE_VAL) {
304
0
        out = proj_coord_error(); /* in the 3D case +t_obs must be specified */
305
0
        proj_log_debug(P, "+dt must be specified");
306
0
        return out.lpz;
307
0
    }
308
309
0
    out.xyz = pj_deformation_reverse_shift(P, in, Q->dt);
310
311
0
    return out.lpz;
312
0
}
313
314
0
static void pj_deformation_reverse_4d(PJ_COORD &coo, PJ *P) {
315
0
    struct deformationData *Q = (struct deformationData *)P->opaque;
316
0
    double dt;
317
318
0
    if (Q->dt != HUGE_VAL) {
319
0
        dt = Q->dt;
320
0
    } else {
321
0
        if (coo.xyzt.t == HUGE_VAL) {
322
0
            coo = proj_coord_error();
323
0
            proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_MISSING_TIME);
324
0
            return;
325
0
        }
326
0
        dt = coo.xyzt.t - Q->t_epoch;
327
0
    }
328
329
0
    coo.xyz = pj_deformation_reverse_shift(P, coo.xyz, dt);
330
0
}
331
332
105
static PJ *pj_deformation_destructor(PJ *P, int errlev) {
333
105
    if (nullptr == P)
334
0
        return nullptr;
335
336
105
    auto Q = static_cast<struct deformationData *>(P->opaque);
337
105
    if (Q) {
338
105
        if (Q->cart)
339
105
            Q->cart->destructor(Q->cart, errlev);
340
105
        delete Q;
341
105
    }
342
105
    P->opaque = nullptr;
343
344
105
    return pj_default_destructor(P, errlev);
345
105
}
346
347
105
PJ *PJ_TRANSFORMATION(deformation, 1) {
348
105
    auto Q = new deformationData;
349
105
    P->opaque = (void *)Q;
350
105
    P->destructor = pj_deformation_destructor;
351
352
    // Pass a dummy ellipsoid definition that will be overridden just afterwards
353
105
    Q->cart = proj_create(P->ctx, "+proj=cart +a=1");
354
105
    if (Q->cart == nullptr)
355
0
        return pj_deformation_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
356
357
    /* inherit ellipsoid definition from P to Q->cart */
358
105
    pj_inherit_ellipsoid_def(P, Q->cart);
359
360
105
    int has_xy_grids = pj_param(P->ctx, P->params, "txy_grids").i;
361
105
    int has_z_grids = pj_param(P->ctx, P->params, "tz_grids").i;
362
105
    int has_grids = pj_param(P->ctx, P->params, "tgrids").i;
363
364
    /* Build gridlists. Both horizontal and vertical grids are mandatory. */
365
105
    if (!has_grids && (!has_xy_grids || !has_z_grids)) {
366
16
        proj_log_error(P, _("Either +grids or (+xy_grids and +z_grids) should "
367
16
                            "be specified."));
368
16
        return pj_deformation_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
369
16
    }
370
371
89
    if (has_grids) {
372
72
        Q->grids = pj_generic_grid_init(P, "grids");
373
        /* Was gridlist compiled properly? */
374
72
        if (proj_errno(P)) {
375
17
            proj_log_error(P, _("could not find required grid(s).)"));
376
17
            return pj_deformation_destructor(
377
17
                P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
378
17
        }
379
72
    } else {
380
17
        Q->hgrids = pj_hgrid_init(P, "xy_grids");
381
17
        if (proj_errno(P)) {
382
13
            proj_log_error(P, _("could not find requested xy_grid(s)."));
383
13
            return pj_deformation_destructor(
384
13
                P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
385
13
        }
386
387
4
        Q->vgrids = pj_vgrid_init(P, "z_grids");
388
4
        if (proj_errno(P)) {
389
3
            proj_log_error(P, _("could not find requested z_grid(s)."));
390
3
            return pj_deformation_destructor(
391
3
                P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
392
3
        }
393
4
    }
394
395
56
    Q->dt = HUGE_VAL;
396
56
    if (pj_param(P->ctx, P->params, "tdt").i) {
397
55
        Q->dt = pj_param(P->ctx, P->params, "ddt").f;
398
55
    }
399
400
56
    if (pj_param_exists(P->params, "t_obs")) {
401
0
        proj_log_error(P,
402
0
                       _("+t_obs parameter is deprecated. Use +dt instead."));
403
0
        return pj_deformation_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
404
0
    }
405
406
56
    Q->t_epoch = HUGE_VAL;
407
56
    if (pj_param(P->ctx, P->params, "tt_epoch").i) {
408
0
        Q->t_epoch = pj_param(P->ctx, P->params, "dt_epoch").f;
409
0
    }
410
411
56
    if (Q->dt == HUGE_VAL && Q->t_epoch == HUGE_VAL) {
412
1
        proj_log_error(P, _("either +dt or +t_epoch needs to be set."));
413
1
        return pj_deformation_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
414
1
    }
415
416
55
    if (Q->dt != HUGE_VALL && Q->t_epoch != HUGE_VALL) {
417
0
        proj_log_error(P, _("+dt or +t_epoch are mutually exclusive."));
418
0
        return pj_deformation_destructor(
419
0
            P, PROJ_ERR_INVALID_OP_MUTUALLY_EXCLUSIVE_ARGS);
420
0
    }
421
422
55
    P->fwd4d = pj_deformation_forward_4d;
423
55
    P->inv4d = pj_deformation_reverse_4d;
424
55
    P->fwd3d = pj_deformation_forward_3d;
425
55
    P->inv3d = pj_deformation_reverse_3d;
426
55
    P->fwd = nullptr;
427
55
    P->inv = nullptr;
428
429
55
    P->left = PJ_IO_UNITS_CARTESIAN;
430
55
    P->right = PJ_IO_UNITS_CARTESIAN;
431
432
55
    return P;
433
55
}
434
435
#undef TOL
436
#undef MAX_ITERATIONS