Coverage Report

Created: 2025-06-13 06:29

/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
0
                                           double &vy, double &vz) {
86
0
    GenericShiftGridSet *gridset = nullptr;
87
0
    auto grid = pj_find_generic_grid(Q->grids, lp, gridset);
88
0
    if (!grid) {
89
0
        return false;
90
0
    }
91
0
    if (grid->isNullGrid()) {
92
0
        vx = 0;
93
0
        vy = 0;
94
0
        vz = 0;
95
0
        return true;
96
0
    }
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
0
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
0
    PJ_COORD geodetic, shift, temp;
151
0
    double sp, cp, sl, cl;
152
0
    int previous_errno = proj_errno_reset(P);
153
0
    auto Q = static_cast<deformationData *>(P->opaque);
154
155
    /* cartesian to geodetic */
156
0
    geodetic.lpz = pj_inv3d(cartesian, Q->cart);
157
158
    /* look up correction values in grids */
159
0
    if (!Q->grids.empty()) {
160
0
        double vx = 0;
161
0
        double vy = 0;
162
0
        double vz = 0;
163
0
        if (!pj_deformation_get_grid_values(P, Q, geodetic.lp, vx, vy, vz)) {
164
0
            return proj_coord_error().xyz;
165
0
        }
166
0
        shift.xyz.x = vx;
167
0
        shift.xyz.y = vy;
168
0
        shift.xyz.z = vz;
169
0
    } 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
0
    sp = sin(geodetic.lpz.phi);
186
0
    cp = cos(geodetic.lpz.phi);
187
0
    sl = sin(geodetic.lpz.lam);
188
0
    cl = cos(geodetic.lpz.lam);
189
190
    /* ENU -> PJ_XYZ */
191
0
    temp.xyz.x =
192
0
        -sp * cl * shift.enu.n - sl * shift.enu.e + cp * cl * shift.enu.u;
193
0
    temp.xyz.y =
194
0
        -sp * sl * shift.enu.n + cl * shift.enu.e + cp * sl * shift.enu.u;
195
0
    temp.xyz.z = cp * shift.enu.n + sp * shift.enu.u;
196
197
0
    shift.xyz = temp.xyz;
198
199
0
    proj_errno_restore(P, previous_errno);
200
201
0
    return shift.xyz;
202
0
}
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
0
static void pj_deformation_forward_4d(PJ_COORD &coo, PJ *P) {
276
0
    struct deformationData *Q = (struct deformationData *)P->opaque;
277
0
    double dt;
278
0
    PJ_XYZ shift;
279
280
0
    if (Q->dt != HUGE_VAL) {
281
0
        dt = Q->dt;
282
0
    } 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
0
    shift = pj_deformation_get_grid_shift(P, coo.xyz);
292
293
0
    coo.xyzt.x += dt * shift.x;
294
0
    coo.xyzt.y += dt * shift.y;
295
0
    coo.xyzt.z += dt * shift.z;
296
0
}
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
84
static PJ *pj_deformation_destructor(PJ *P, int errlev) {
333
84
    if (nullptr == P)
334
0
        return nullptr;
335
336
84
    auto Q = static_cast<struct deformationData *>(P->opaque);
337
84
    if (Q) {
338
84
        if (Q->cart)
339
84
            Q->cart->destructor(Q->cart, errlev);
340
84
        delete Q;
341
84
    }
342
84
    P->opaque = nullptr;
343
344
84
    return pj_default_destructor(P, errlev);
345
84
}
346
347
84
PJ *PJ_TRANSFORMATION(deformation, 1) {
348
84
    auto Q = new deformationData;
349
84
    P->opaque = (void *)Q;
350
84
    P->destructor = pj_deformation_destructor;
351
352
    // Pass a dummy ellipsoid definition that will be overridden just afterwards
353
84
    Q->cart = proj_create(P->ctx, "+proj=cart +a=1");
354
84
    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
84
    pj_inherit_ellipsoid_def(P, Q->cart);
359
360
84
    int has_xy_grids = pj_param(P->ctx, P->params, "txy_grids").i;
361
84
    int has_z_grids = pj_param(P->ctx, P->params, "tz_grids").i;
362
84
    int has_grids = pj_param(P->ctx, P->params, "tgrids").i;
363
364
    /* Build gridlists. Both horizontal and vertical grids are mandatory. */
365
84
    if (!has_grids && (!has_xy_grids || !has_z_grids)) {
366
5
        proj_log_error(P, _("Either +grids or (+xy_grids and +z_grids) should "
367
5
                            "be specified."));
368
5
        return pj_deformation_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
369
5
    }
370
371
79
    if (has_grids) {
372
79
        Q->grids = pj_generic_grid_init(P, "grids");
373
        /* Was gridlist compiled properly? */
374
79
        if (proj_errno(P)) {
375
3
            proj_log_error(P, _("could not find required grid(s).)"));
376
3
            return pj_deformation_destructor(
377
3
                P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
378
3
        }
379
79
    } else {
380
0
        Q->hgrids = pj_hgrid_init(P, "xy_grids");
381
0
        if (proj_errno(P)) {
382
0
            proj_log_error(P, _("could not find requested xy_grid(s)."));
383
0
            return pj_deformation_destructor(
384
0
                P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
385
0
        }
386
387
0
        Q->vgrids = pj_vgrid_init(P, "z_grids");
388
0
        if (proj_errno(P)) {
389
0
            proj_log_error(P, _("could not find requested z_grid(s)."));
390
0
            return pj_deformation_destructor(
391
0
                P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
392
0
        }
393
0
    }
394
395
76
    Q->dt = HUGE_VAL;
396
76
    if (pj_param(P->ctx, P->params, "tdt").i) {
397
68
        Q->dt = pj_param(P->ctx, P->params, "ddt").f;
398
68
    }
399
400
76
    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
76
    Q->t_epoch = HUGE_VAL;
407
76
    if (pj_param(P->ctx, P->params, "tt_epoch").i) {
408
2
        Q->t_epoch = pj_param(P->ctx, P->params, "dt_epoch").f;
409
2
    }
410
411
76
    if (Q->dt == HUGE_VAL && Q->t_epoch == HUGE_VAL) {
412
6
        proj_log_error(P, _("either +dt or +t_epoch needs to be set."));
413
6
        return pj_deformation_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
414
6
    }
415
416
70
    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
70
    P->fwd4d = pj_deformation_forward_4d;
423
70
    P->inv4d = pj_deformation_reverse_4d;
424
70
    P->fwd3d = pj_deformation_forward_3d;
425
70
    P->inv3d = pj_deformation_reverse_3d;
426
70
    P->fwd = nullptr;
427
70
    P->inv = nullptr;
428
429
70
    P->left = PJ_IO_UNITS_CARTESIAN;
430
70
    P->right = PJ_IO_UNITS_CARTESIAN;
431
432
70
    return P;
433
70
}
434
435
#undef TOL
436
#undef MAX_ITERATIONS