Coverage Report

Created: 2025-06-13 06:29

/src/proj/src/transformations/gridshift.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 * Project:  PROJ
3
 * Purpose:  Generic grid shifting, in particular Geographic 3D offsets
4
 * Author:   Even Rouault, <even.rouault at spatialys.com>
5
 *
6
 ******************************************************************************
7
 * Copyright (c) 2022, Even Rouault, <even.rouault at spatialys.com>
8
 *
9
 * Permission is hereby granted, free of charge, to any person obtaining a
10
 * copy of this software and associated documentation files (the "Software"),
11
 * to deal in the Software without restriction, including without limitation
12
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13
 * and/or sell copies of the Software, and to permit persons to whom the
14
 * Software is furnished to do so, subject to the following conditions:
15
 *
16
 * The above copyright notice and this permission notice shall be included
17
 * in all copies or substantial portions of the Software.
18
 *
19
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25
 * DEALINGS IN THE SOFTWARE.
26
 *****************************************************************************/
27
28
#ifndef FROM_PROJ_CPP
29
#define FROM_PROJ_CPP
30
#endif
31
32
#include <errno.h>
33
#include <mutex>
34
#include <stddef.h>
35
#include <string.h>
36
#include <time.h>
37
38
#include "grids.hpp"
39
#include "proj/internal/internal.hpp"
40
#include "proj_internal.h"
41
42
#include <algorithm>
43
#include <cmath>
44
#include <limits>
45
#include <map>
46
#include <utility>
47
48
PROJ_HEAD(gridshift, "Generic grid shift");
49
50
static std::mutex gMutex{};
51
// Map of (name, isProjected)
52
static std::map<std::string, bool> gKnownGrids{};
53
54
using namespace NS_PROJ;
55
56
namespace { // anonymous namespace
57
58
struct IXY {
59
    int32_t x, y;
60
61
0
    inline bool operator!=(const IXY &other) const {
62
0
        return x != other.x || y != other.y;
63
0
    }
64
};
65
66
struct GridInfo {
67
    int idxSampleX = -1;
68
    int idxSampleY = -1;
69
    int idxSampleZ = -1;
70
    bool eastingNorthingOffset = false;
71
    bool bilinearInterpolation = true;
72
    std::vector<float> shifts{};
73
    bool swapXYInRes = false;
74
    std::vector<int> idxSampleXYZ{-1, -1, -1};
75
    IXY lastIdxXY = IXY{-1, -1};
76
};
77
78
// ---------------------------------------------------------------------------
79
80
struct gridshiftData {
81
    ListOfGenericGrids m_grids{};
82
    bool m_defer_grid_opening = false;
83
    int m_error_code_in_defer_grid_opening = 0;
84
    bool m_bHasHorizontalOffset = false;
85
    bool m_bHasGeographic3DOffset = false;
86
    bool m_bHasEllipsoidalHeightOffset = false;
87
    bool m_bHasVerticalToVertical = false;
88
    bool m_bHasGeographicToVertical = false;
89
    bool m_mainGridTypeIsGeographic3DOffset = false;
90
    bool m_skip_z_transform = false;
91
    std::string m_mainGridType{};
92
    std::string m_auxGridType{};
93
    std::string m_interpolation{};
94
    std::map<const GenericShiftGrid *, GridInfo> m_cacheGridInfo{};
95
96
    //! Offset in X to add in the forward direction, after the correction
97
    // has been applied. (and reciprocally to subtract in the inverse direction
98
    // before reading the grid). Used typically for the S-JTSK --> S-JTSK/05
99
    // grid
100
    double m_offsetX = 0;
101
102
    //! Offset in Y to add in the forward direction, after the correction
103
    // has been applied. (and reciprocally to subtract in the inverse direction
104
    // before reading the grid). Used typically for the S-JTSK --> S-JTSK/05
105
    // grid
106
    double m_offsetY = 0;
107
108
    bool checkGridTypes(PJ *P, bool &isProjectedCoord);
109
    bool loadGridsIfNeeded(PJ *P);
110
    const GenericShiftGrid *findGrid(const std::string &type,
111
                                     const PJ_XYZ &input,
112
                                     GenericShiftGridSet *&gridSetOut) const;
113
    PJ_XYZ grid_interpolate(PJ_CONTEXT *ctx, const std::string &type, PJ_XY xy,
114
                            const GenericShiftGrid *grid,
115
                            bool &biquadraticInterpolationOut);
116
    PJ_XYZ grid_apply_internal(PJ_CONTEXT *ctx, const std::string &type,
117
                               bool isVerticalOnly, const PJ_XYZ in,
118
                               PJ_DIRECTION direction,
119
                               const GenericShiftGrid *grid,
120
                               GenericShiftGridSet *gridset, bool &shouldRetry);
121
122
    PJ_XYZ apply(PJ *P, PJ_DIRECTION dir, PJ_XYZ xyz);
123
};
124
125
// ---------------------------------------------------------------------------
126
127
169
bool gridshiftData::checkGridTypes(PJ *P, bool &isProjectedCoord) {
128
169
    std::string offsetX, offsetY;
129
169
    int gridCount = 0;
130
169
    isProjectedCoord = false;
131
169
    for (const auto &gridset : m_grids) {
132
11
        for (const auto &grid : gridset->grids()) {
133
11
            ++gridCount;
134
11
            const auto &type = grid->metadataItem("TYPE");
135
11
            if (type == "HORIZONTAL_OFFSET") {
136
0
                m_bHasHorizontalOffset = true;
137
0
                if (offsetX.empty()) {
138
0
                    offsetX = grid->metadataItem("constant_offset", 0);
139
0
                }
140
0
                if (offsetY.empty()) {
141
0
                    offsetY = grid->metadataItem("constant_offset", 1);
142
0
                }
143
11
            } else if (type == "GEOGRAPHIC_3D_OFFSET")
144
0
                m_bHasGeographic3DOffset = true;
145
11
            else if (type == "ELLIPSOIDAL_HEIGHT_OFFSET")
146
0
                m_bHasEllipsoidalHeightOffset = true;
147
11
            else if (type == "VERTICAL_OFFSET_VERTICAL_TO_VERTICAL")
148
0
                m_bHasVerticalToVertical = true;
149
11
            else if (type == "VERTICAL_OFFSET_GEOGRAPHIC_TO_VERTICAL")
150
0
                m_bHasGeographicToVertical = true;
151
11
            else if (type.empty()) {
152
11
                proj_log_error(P, _("Missing TYPE metadata item in grid(s)."));
153
11
                return false;
154
11
            } else {
155
0
                proj_log_error(
156
0
                    P, _("Unhandled value for TYPE metadata item in grid(s)."));
157
0
                return false;
158
0
            }
159
160
0
            isProjectedCoord = !grid->extentAndRes().isGeographic;
161
0
        }
162
11
    }
163
164
158
    if (!offsetX.empty() || !offsetY.empty()) {
165
0
        if (gridCount > 1) {
166
            // Makes life easier...
167
0
            proj_log_error(P, _("Shift offset found in one grid. Only one grid "
168
0
                                "with shift offset is supported at a time."));
169
0
            return false;
170
0
        }
171
0
        try {
172
0
            m_offsetX = NS_PROJ::internal::c_locale_stod(offsetX);
173
0
        } catch (const std::exception &) {
174
0
            proj_log_error(P, _("Invalid offset value"));
175
0
            return false;
176
0
        }
177
0
        try {
178
0
            m_offsetY = NS_PROJ::internal::c_locale_stod(offsetY);
179
0
        } catch (const std::exception &) {
180
0
            proj_log_error(P, _("Invalid offset value"));
181
0
            return false;
182
0
        }
183
0
    }
184
185
158
    if (((m_bHasEllipsoidalHeightOffset ? 1 : 0) +
186
158
         (m_bHasVerticalToVertical ? 1 : 0) +
187
158
         (m_bHasGeographicToVertical ? 1 : 0)) > 1) {
188
0
        proj_log_error(P, _("Unsupported mix of grid types."));
189
0
        return false;
190
0
    }
191
192
158
    if (m_bHasGeographic3DOffset) {
193
0
        m_mainGridTypeIsGeographic3DOffset = true;
194
0
        m_mainGridType = "GEOGRAPHIC_3D_OFFSET";
195
158
    } else if (!m_bHasHorizontalOffset) {
196
158
        if (m_bHasEllipsoidalHeightOffset)
197
0
            m_mainGridType = "ELLIPSOIDAL_HEIGHT_OFFSET";
198
158
        else if (m_bHasGeographicToVertical)
199
0
            m_mainGridType = "VERTICAL_OFFSET_GEOGRAPHIC_TO_VERTICAL";
200
158
        else {
201
158
            assert(m_bHasVerticalToVertical);
202
158
            m_mainGridType = "VERTICAL_OFFSET_VERTICAL_TO_VERTICAL";
203
158
        }
204
158
    } else {
205
0
        assert(m_bHasHorizontalOffset);
206
0
        m_mainGridType = "HORIZONTAL_OFFSET";
207
0
    }
208
209
158
    if (m_bHasHorizontalOffset) {
210
0
        if (m_bHasEllipsoidalHeightOffset)
211
0
            m_auxGridType = "ELLIPSOIDAL_HEIGHT_OFFSET";
212
0
        else if (m_bHasGeographicToVertical)
213
0
            m_auxGridType = "VERTICAL_OFFSET_GEOGRAPHIC_TO_VERTICAL";
214
0
        else if (m_bHasVerticalToVertical) {
215
0
            m_auxGridType = "VERTICAL_OFFSET_VERTICAL_TO_VERTICAL";
216
0
        }
217
0
    }
218
219
158
    return true;
220
158
}
221
222
// ---------------------------------------------------------------------------
223
224
const GenericShiftGrid *
225
gridshiftData::findGrid(const std::string &type, const PJ_XYZ &input,
226
0
                        GenericShiftGridSet *&gridSetOut) const {
227
0
    for (const auto &gridset : m_grids) {
228
0
        auto grid = gridset->gridAt(type, input.x, input.y);
229
0
        if (grid) {
230
0
            gridSetOut = gridset.get();
231
0
            return grid;
232
0
        }
233
0
    }
234
0
    return nullptr;
235
0
}
236
237
// ---------------------------------------------------------------------------
238
239
0
#define REL_TOLERANCE_HGRIDSHIFT 1e-5
240
241
PJ_XYZ gridshiftData::grid_interpolate(PJ_CONTEXT *ctx, const std::string &type,
242
                                       PJ_XY xy, const GenericShiftGrid *grid,
243
0
                                       bool &biquadraticInterpolationOut) {
244
0
    PJ_XYZ val;
245
246
0
    val.x = val.y = HUGE_VAL;
247
0
    val.z = 0;
248
249
0
    const bool isProjectedCoord = !grid->extentAndRes().isGeographic;
250
0
    auto iterCache = m_cacheGridInfo.find(grid);
251
0
    if (iterCache == m_cacheGridInfo.end()) {
252
0
        bool eastingNorthingOffset = false;
253
0
        const auto samplesPerPixel = grid->samplesPerPixel();
254
0
        int idxSampleY = -1;
255
0
        int idxSampleX = -1;
256
0
        int idxSampleZ = -1;
257
0
        for (int i = 0; i < samplesPerPixel; i++) {
258
0
            const auto desc = grid->description(i);
259
0
            if (!isProjectedCoord && desc == "latitude_offset") {
260
0
                idxSampleY = i;
261
0
                const auto unit = grid->unit(idxSampleY);
262
0
                if (!unit.empty() && unit != "arc-second") {
263
0
                    pj_log(ctx, PJ_LOG_ERROR,
264
0
                           "gridshift: Only unit=arc-second currently handled");
265
0
                    return val;
266
0
                }
267
0
            } else if (!isProjectedCoord && desc == "longitude_offset") {
268
0
                idxSampleX = i;
269
0
                const auto unit = grid->unit(idxSampleX);
270
0
                if (!unit.empty() && unit != "arc-second") {
271
0
                    pj_log(ctx, PJ_LOG_ERROR,
272
0
                           "gridshift: Only unit=arc-second currently handled");
273
0
                    return val;
274
0
                }
275
0
            } else if (isProjectedCoord && desc == "easting_offset") {
276
0
                eastingNorthingOffset = true;
277
0
                idxSampleX = i;
278
0
                const auto unit = grid->unit(idxSampleX);
279
0
                if (!unit.empty() && unit != "metre") {
280
0
                    pj_log(ctx, PJ_LOG_ERROR,
281
0
                           "gridshift: Only unit=metre currently handled");
282
0
                    return val;
283
0
                }
284
0
            } else if (isProjectedCoord && desc == "northing_offset") {
285
0
                eastingNorthingOffset = true;
286
0
                idxSampleY = i;
287
0
                const auto unit = grid->unit(idxSampleY);
288
0
                if (!unit.empty() && unit != "metre") {
289
0
                    pj_log(ctx, PJ_LOG_ERROR,
290
0
                           "gridshift: Only unit=metre currently handled");
291
0
                    return val;
292
0
                }
293
0
            } else if (desc == "ellipsoidal_height_offset" ||
294
0
                       desc == "geoid_undulation" || desc == "hydroid_height" ||
295
0
                       desc == "vertical_offset") {
296
0
                idxSampleZ = i;
297
0
                const auto unit = grid->unit(idxSampleZ);
298
0
                if (!unit.empty() && unit != "metre") {
299
0
                    pj_log(ctx, PJ_LOG_ERROR,
300
0
                           "gridshift: Only unit=metre currently handled");
301
0
                    return val;
302
0
                }
303
0
            }
304
0
        }
305
0
        if (samplesPerPixel >= 2 && idxSampleY < 0 && idxSampleX < 0 &&
306
0
            type == "HORIZONTAL_OFFSET") {
307
0
            if (isProjectedCoord) {
308
0
                eastingNorthingOffset = true;
309
0
                idxSampleX = 0;
310
0
                idxSampleY = 1;
311
0
            } else {
312
                // X=longitude assumed to be the second component if metadata
313
                // lacking
314
0
                idxSampleX = 1;
315
                // Y=latitude assumed to be the first component if metadata
316
                // lacking
317
0
                idxSampleY = 0;
318
0
            }
319
0
        }
320
0
        if (type == "HORIZONTAL_OFFSET" || type == "GEOGRAPHIC_3D_OFFSET") {
321
0
            if (idxSampleY < 0 || idxSampleX < 0) {
322
0
                pj_log(ctx, PJ_LOG_ERROR,
323
0
                       "gridshift: grid has not expected samples");
324
0
                return val;
325
0
            }
326
0
        }
327
0
        if (type == "ELLIPSOIDAL_HEIGHT_OFFSET" ||
328
0
            type == "VERTICAL_OFFSET_GEOGRAPHIC_TO_VERTICAL" ||
329
0
            type == "VERTICAL_OFFSET_VERTICAL_TO_VERTICAL" ||
330
0
            type == "GEOGRAPHIC_3D_OFFSET") {
331
0
            if (idxSampleZ < 0) {
332
0
                pj_log(ctx, PJ_LOG_ERROR,
333
0
                       "gridshift: grid has not expected samples");
334
0
                return val;
335
0
            }
336
0
        }
337
338
0
        std::string interpolation(m_interpolation);
339
0
        if (interpolation.empty())
340
0
            interpolation = grid->metadataItem("interpolation_method");
341
0
        if (interpolation.empty())
342
0
            interpolation = "bilinear";
343
344
0
        if (interpolation != "bilinear" && interpolation != "biquadratic") {
345
0
            pj_log(ctx, PJ_LOG_ERROR,
346
0
                   "gridshift: Unsupported interpolation_method in grid");
347
0
            return val;
348
0
        }
349
350
0
        GridInfo gridInfo;
351
0
        gridInfo.idxSampleX = idxSampleX;
352
0
        gridInfo.idxSampleY = idxSampleY;
353
0
        gridInfo.idxSampleZ = m_skip_z_transform ? -1 : idxSampleZ;
354
0
        gridInfo.eastingNorthingOffset = eastingNorthingOffset;
355
0
        gridInfo.bilinearInterpolation =
356
0
            (interpolation == "bilinear" || grid->width() < 3 ||
357
0
             grid->height() < 3);
358
0
        gridInfo.shifts.resize(3 * 3 * 3);
359
0
        if (idxSampleX == 1 && idxSampleY == 0) {
360
            // Little optimization for the common of grids storing shifts in
361
            // latitude, longitude, in that order.
362
            // We want to request data in the order it is stored in the grid,
363
            // which triggers a read optimization.
364
            // But we must compensate for that by switching the role of x and y
365
            // after computation.
366
0
            gridInfo.swapXYInRes = true;
367
0
            gridInfo.idxSampleXYZ[0] = 0;
368
0
            gridInfo.idxSampleXYZ[1] = 1;
369
0
        } else {
370
0
            gridInfo.idxSampleXYZ[0] = idxSampleX;
371
0
            gridInfo.idxSampleXYZ[1] = idxSampleY;
372
0
        }
373
0
        gridInfo.idxSampleXYZ[2] = idxSampleZ;
374
0
        iterCache = m_cacheGridInfo.emplace(grid, std::move(gridInfo)).first;
375
0
    }
376
    // cppcheck-suppress derefInvalidIteratorRedundantCheck
377
0
    GridInfo &gridInfo = iterCache->second;
378
0
    const int idxSampleX = gridInfo.idxSampleX;
379
0
    const int idxSampleY = gridInfo.idxSampleY;
380
0
    const int idxSampleZ = gridInfo.idxSampleZ;
381
0
    const bool bilinearInterpolation = gridInfo.bilinearInterpolation;
382
0
    biquadraticInterpolationOut = !bilinearInterpolation;
383
384
0
    IXY indxy;
385
0
    const auto &extent = grid->extentAndRes();
386
0
    double x = (xy.x - extent.west) / extent.resX;
387
0
    indxy.x = std::isnan(x) ? 0 : (int32_t)lround(floor(x));
388
0
    double y = (xy.y - extent.south) / extent.resY;
389
0
    indxy.y = std::isnan(y) ? 0 : (int32_t)lround(floor(y));
390
391
0
    PJ_XY frct;
392
0
    frct.x = x - indxy.x;
393
0
    frct.y = y - indxy.y;
394
0
    int tmpInt;
395
0
    if (indxy.x < 0) {
396
0
        if (indxy.x == -1 && frct.x > 1 - 10 * REL_TOLERANCE_HGRIDSHIFT) {
397
0
            ++indxy.x;
398
0
            frct.x = 0.;
399
0
        } else
400
0
            return val;
401
0
    } else if ((tmpInt = indxy.x + 1) >= grid->width()) {
402
0
        if (tmpInt == grid->width() && frct.x < 10 * REL_TOLERANCE_HGRIDSHIFT) {
403
0
            --indxy.x;
404
0
            frct.x = 1.;
405
0
        } else
406
0
            return val;
407
0
    }
408
0
    if (indxy.y < 0) {
409
0
        if (indxy.y == -1 && frct.y > 1 - 10 * REL_TOLERANCE_HGRIDSHIFT) {
410
0
            ++indxy.y;
411
0
            frct.y = 0.;
412
0
        } else
413
0
            return val;
414
0
    } else if ((tmpInt = indxy.y + 1) >= grid->height()) {
415
0
        if (tmpInt == grid->height() &&
416
0
            frct.y < 10 * REL_TOLERANCE_HGRIDSHIFT) {
417
0
            --indxy.y;
418
0
            frct.y = 1.;
419
0
        } else
420
0
            return val;
421
0
    }
422
423
0
    bool nodataFound = false;
424
0
    if (bilinearInterpolation) {
425
0
        double m10 = frct.x;
426
0
        double m11 = m10;
427
0
        double m01 = 1. - frct.x;
428
0
        double m00 = m01;
429
0
        m11 *= frct.y;
430
0
        m01 *= frct.y;
431
0
        frct.y = 1. - frct.y;
432
0
        m00 *= frct.y;
433
0
        m10 *= frct.y;
434
0
        if (idxSampleX >= 0 && idxSampleY >= 0) {
435
0
            if (gridInfo.lastIdxXY != indxy) {
436
0
                if (!grid->valuesAt(indxy.x, indxy.y, 2, 2,
437
0
                                    idxSampleZ >= 0 ? 3 : 2,
438
0
                                    gridInfo.idxSampleXYZ.data(),
439
0
                                    gridInfo.shifts.data(), nodataFound) ||
440
0
                    nodataFound) {
441
0
                    return val;
442
0
                }
443
0
                gridInfo.lastIdxXY = indxy;
444
0
            }
445
0
            if (idxSampleZ >= 0) {
446
0
                val.x = (m00 * gridInfo.shifts[0] + m10 * gridInfo.shifts[3] +
447
0
                         m01 * gridInfo.shifts[6] + m11 * gridInfo.shifts[9]);
448
0
                val.y = (m00 * gridInfo.shifts[1] + m10 * gridInfo.shifts[4] +
449
0
                         m01 * gridInfo.shifts[7] + m11 * gridInfo.shifts[10]);
450
0
                val.z = m00 * gridInfo.shifts[2] + m10 * gridInfo.shifts[5] +
451
0
                        m01 * gridInfo.shifts[8] + m11 * gridInfo.shifts[11];
452
0
            } else {
453
0
                val.x = (m00 * gridInfo.shifts[0] + m10 * gridInfo.shifts[2] +
454
0
                         m01 * gridInfo.shifts[4] + m11 * gridInfo.shifts[6]);
455
0
                val.y = (m00 * gridInfo.shifts[1] + m10 * gridInfo.shifts[3] +
456
0
                         m01 * gridInfo.shifts[5] + m11 * gridInfo.shifts[7]);
457
0
            }
458
0
        } else {
459
0
            val.x = 0;
460
0
            val.y = 0;
461
0
            if (idxSampleZ >= 0) {
462
0
                if (gridInfo.lastIdxXY != indxy) {
463
0
                    if (!grid->valuesAt(indxy.x, indxy.y, 2, 2, 1, &idxSampleZ,
464
0
                                        gridInfo.shifts.data(), nodataFound) ||
465
0
                        nodataFound) {
466
0
                        return val;
467
0
                    }
468
0
                    gridInfo.lastIdxXY = indxy;
469
0
                }
470
0
                val.z = m00 * gridInfo.shifts[0] + m10 * gridInfo.shifts[1] +
471
0
                        m01 * gridInfo.shifts[2] + m11 * gridInfo.shifts[3];
472
0
            }
473
0
        }
474
0
    } else // biquadratic
475
0
    {
476
        // Cf https://geodesy.noaa.gov/library/pdfs/NOAA_TM_NOS_NGS_0084.pdf
477
        // Depending if we are before or after half-pixel, shift the 3x3 window
478
        // of interpolation
479
0
        if ((frct.x <= 0.5 && indxy.x > 0) || (indxy.x + 2 == grid->width())) {
480
0
            indxy.x -= 1;
481
0
            frct.x += 1;
482
0
        }
483
0
        if ((frct.y <= 0.5 && indxy.y > 0) || (indxy.y + 2 == grid->height())) {
484
0
            indxy.y -= 1;
485
0
            frct.y += 1;
486
0
        }
487
488
        // Port of qterp() Fortran function from NOAA
489
        // xToInterp must be in [0,2] range
490
        // f0 must be f(0), f1 must be f(1), f2 must be f(2)
491
        // Returns f(xToInterp) interpolated value along the parabolic function
492
0
        const auto quadraticInterpol = [](double xToInterp, double f0,
493
0
                                          double f1, double f2) {
494
0
            const double df0 = f1 - f0;
495
0
            const double df1 = f2 - f1;
496
0
            const double d2f0 = df1 - df0;
497
0
            return f0 + xToInterp * df0 +
498
0
                   0.5 * xToInterp * (xToInterp - 1.0) * d2f0;
499
0
        };
500
501
0
        if (idxSampleX >= 0 && idxSampleY >= 0) {
502
0
            if (gridInfo.lastIdxXY != indxy) {
503
0
                if (!grid->valuesAt(indxy.x, indxy.y, 3, 3,
504
0
                                    idxSampleZ >= 0 ? 3 : 2,
505
0
                                    gridInfo.idxSampleXYZ.data(),
506
0
                                    gridInfo.shifts.data(), nodataFound) ||
507
0
                    nodataFound) {
508
0
                    return val;
509
0
                }
510
0
                gridInfo.lastIdxXY = indxy;
511
0
            }
512
0
            const auto *shifts_ptr = gridInfo.shifts.data();
513
0
            if (idxSampleZ >= 0) {
514
0
                double xyz_shift[3][4];
515
0
                for (int j = 0; j <= 2; ++j) {
516
0
                    xyz_shift[j][0] = quadraticInterpol(
517
0
                        frct.x, shifts_ptr[0], shifts_ptr[3], shifts_ptr[6]);
518
0
                    xyz_shift[j][1] = quadraticInterpol(
519
0
                        frct.x, shifts_ptr[1], shifts_ptr[4], shifts_ptr[7]);
520
0
                    xyz_shift[j][2] = quadraticInterpol(
521
0
                        frct.x, shifts_ptr[2], shifts_ptr[5], shifts_ptr[8]);
522
0
                    shifts_ptr += 9;
523
0
                }
524
0
                val.x = quadraticInterpol(frct.y, xyz_shift[0][0],
525
0
                                          xyz_shift[1][0], xyz_shift[2][0]);
526
0
                val.y = quadraticInterpol(frct.y, xyz_shift[0][1],
527
0
                                          xyz_shift[1][1], xyz_shift[2][1]);
528
0
                val.z = quadraticInterpol(frct.y, xyz_shift[0][2],
529
0
                                          xyz_shift[1][2], xyz_shift[2][2]);
530
0
            } else {
531
0
                double xy_shift[3][2];
532
0
                for (int j = 0; j <= 2; ++j) {
533
0
                    xy_shift[j][0] = quadraticInterpol(
534
0
                        frct.x, shifts_ptr[0], shifts_ptr[2], shifts_ptr[4]);
535
0
                    xy_shift[j][1] = quadraticInterpol(
536
0
                        frct.x, shifts_ptr[1], shifts_ptr[3], shifts_ptr[5]);
537
0
                    shifts_ptr += 6;
538
0
                }
539
0
                val.x = quadraticInterpol(frct.y, xy_shift[0][0],
540
0
                                          xy_shift[1][0], xy_shift[2][0]);
541
0
                val.y = quadraticInterpol(frct.y, xy_shift[0][1],
542
0
                                          xy_shift[1][1], xy_shift[2][1]);
543
0
            }
544
0
        } else {
545
0
            val.x = 0;
546
0
            val.y = 0;
547
0
            if (idxSampleZ >= 0) {
548
0
                if (gridInfo.lastIdxXY != indxy) {
549
0
                    if (!grid->valuesAt(indxy.x, indxy.y, 3, 3, 1, &idxSampleZ,
550
0
                                        gridInfo.shifts.data(), nodataFound) ||
551
0
                        nodataFound) {
552
0
                        return val;
553
0
                    }
554
0
                    gridInfo.lastIdxXY = indxy;
555
0
                }
556
0
                double z_shift[3];
557
0
                const auto *shifts_ptr = gridInfo.shifts.data();
558
0
                for (int j = 0; j <= 2; ++j) {
559
0
                    z_shift[j] = quadraticInterpol(
560
0
                        frct.x, shifts_ptr[0], shifts_ptr[1], shifts_ptr[2]);
561
0
                    shifts_ptr += 3;
562
0
                }
563
0
                val.z = quadraticInterpol(frct.y, z_shift[0], z_shift[1],
564
0
                                          z_shift[2]);
565
0
            }
566
0
        }
567
0
    }
568
569
0
    if (idxSampleX >= 0 && idxSampleY >= 0 && !gridInfo.eastingNorthingOffset) {
570
0
        constexpr double convFactorXY = 1. / 3600 / 180 * M_PI;
571
0
        val.x *= convFactorXY;
572
0
        val.y *= convFactorXY;
573
0
    }
574
575
0
    if (gridInfo.swapXYInRes) {
576
0
        std::swap(val.x, val.y);
577
0
    }
578
579
0
    return val;
580
0
}
581
582
// ---------------------------------------------------------------------------
583
584
static PJ_XY normalizeX(const GenericShiftGrid *grid, const PJ_XYZ in,
585
0
                        const NS_PROJ::ExtentAndRes *&extentOut) {
586
0
    PJ_XY normalized;
587
0
    normalized.x = in.x;
588
0
    normalized.y = in.y;
589
0
    extentOut = &(grid->extentAndRes());
590
0
    if (extentOut->isGeographic) {
591
0
        const double epsilon =
592
0
            (extentOut->resX + extentOut->resY) * REL_TOLERANCE_HGRIDSHIFT;
593
0
        if (normalized.x < extentOut->west - epsilon)
594
0
            normalized.x += 2 * M_PI;
595
0
        else if (normalized.x > extentOut->east + epsilon)
596
0
            normalized.x -= 2 * M_PI;
597
0
    }
598
0
    return normalized;
599
0
}
600
601
// ---------------------------------------------------------------------------
602
603
0
#define MAX_ITERATIONS 10
604
0
#define TOL 1e-12
605
606
PJ_XYZ gridshiftData::grid_apply_internal(
607
    PJ_CONTEXT *ctx, const std::string &type, bool isVerticalOnly,
608
    const PJ_XYZ in, PJ_DIRECTION direction, const GenericShiftGrid *grid,
609
0
    GenericShiftGridSet *gridset, bool &shouldRetry) {
610
611
0
    shouldRetry = false;
612
0
    if (in.x == HUGE_VAL)
613
0
        return in;
614
615
    /* normalized longitude of input */
616
0
    const NS_PROJ::ExtentAndRes *extent;
617
0
    PJ_XY normalized_in = normalizeX(grid, in, extent);
618
619
0
    bool biquadraticInterpolationOut = false;
620
0
    PJ_XYZ shift = grid_interpolate(ctx, type, normalized_in, grid,
621
0
                                    biquadraticInterpolationOut);
622
0
    if (grid->hasChanged()) {
623
0
        shouldRetry = gridset->reopen(ctx);
624
0
        PJ_XYZ out;
625
0
        out.x = out.y = out.z = HUGE_VAL;
626
0
        return out;
627
0
    }
628
0
    if (shift.x == HUGE_VAL)
629
0
        return shift;
630
631
0
    if (direction == PJ_FWD) {
632
0
        PJ_XYZ out = in;
633
0
        out.x += shift.x;
634
0
        out.y += shift.y;
635
0
        out.z += shift.z;
636
0
        return out;
637
0
    }
638
639
0
    if (isVerticalOnly) {
640
0
        PJ_XYZ out = in;
641
0
        out.z -= shift.z;
642
0
        return out;
643
0
    }
644
645
0
    PJ_XY guess;
646
0
    guess.x = normalized_in.x - shift.x;
647
0
    guess.y = normalized_in.y - shift.y;
648
649
    // NOAA NCAT transformer tool doesn't do iteration in the reverse path.
650
    // Do the same (only for biquadratic, although NCAT applies this logic to
651
    // bilinear too)
652
    // Cf
653
    // https://github.com/noaa-ngs/ncat-lib/blob/77bcff1ce4a78fe06d0312102ada008aefcc2c62/src/gov/noaa/ngs/grid/Transformer.java#L374
654
    // When trying to do iterative reverse path with biquadratic, we can
655
    // get convergence failures on points that are close to the boundary of
656
    // cells or half-cells. For example with
657
    // echo -122.4250009683  37.8286740788 0 | bin/cct -I +proj=gridshift
658
    // +grids=tests/us_noaa_nadcon5_nad83_1986_nad83_harn_conus_extract_sanfrancisco.tif
659
    // +interpolation=biquadratic
660
0
    if (!biquadraticInterpolationOut) {
661
0
        int i = MAX_ITERATIONS;
662
0
        const double toltol = TOL * TOL;
663
0
        PJ_XY diff;
664
0
        do {
665
0
            shift = grid_interpolate(ctx, type, guess, grid,
666
0
                                     biquadraticInterpolationOut);
667
0
            if (grid->hasChanged()) {
668
0
                shouldRetry = gridset->reopen(ctx);
669
0
                PJ_XYZ out;
670
0
                out.x = out.y = out.z = HUGE_VAL;
671
0
                return out;
672
0
            }
673
674
            /* We can possibly go outside of the initial guessed grid, so try */
675
            /* to fetch a new grid into which iterate... */
676
0
            if (shift.x == HUGE_VAL) {
677
0
                PJ_XYZ lp;
678
0
                lp.x = guess.x;
679
0
                lp.y = guess.y;
680
0
                auto newGrid = findGrid(type, lp, gridset);
681
0
                if (newGrid == nullptr || newGrid == grid ||
682
0
                    newGrid->isNullGrid())
683
0
                    break;
684
0
                pj_log(ctx, PJ_LOG_TRACE, "Switching from grid %s to grid %s",
685
0
                       grid->name().c_str(), newGrid->name().c_str());
686
0
                grid = newGrid;
687
0
                normalized_in = normalizeX(grid, in, extent);
688
0
                diff.x = std::numeric_limits<double>::max();
689
0
                diff.y = std::numeric_limits<double>::max();
690
0
                continue;
691
0
            }
692
693
0
            diff.x = guess.x + shift.x - normalized_in.x;
694
0
            diff.y = guess.y + shift.y - normalized_in.y;
695
0
            guess.x -= diff.x;
696
0
            guess.y -= diff.y;
697
698
0
        } while (--i && (diff.x * diff.x + diff.y * diff.y >
699
0
                         toltol)); /* prob. slightly faster than hypot() */
700
701
0
        if (i == 0) {
702
0
            pj_log(ctx, PJ_LOG_TRACE,
703
0
                   "Inverse grid shift iterator failed to converge.");
704
0
            proj_context_errno_set(ctx, PROJ_ERR_COORD_TRANSFM_NO_CONVERGENCE);
705
0
            PJ_XYZ out;
706
0
            out.x = out.y = out.z = HUGE_VAL;
707
0
            return out;
708
0
        }
709
710
0
        if (shift.x == HUGE_VAL) {
711
0
            pj_log(
712
0
                ctx, PJ_LOG_TRACE,
713
0
                "Inverse grid shift iteration failed, presumably at grid edge. "
714
0
                "Using first approximation.");
715
0
        }
716
0
    }
717
718
0
    PJ_XYZ out;
719
0
    out.x = extent->isGeographic ? adjlon(guess.x) : guess.x;
720
0
    out.y = guess.y;
721
0
    out.z = in.z - shift.z;
722
0
    return out;
723
0
}
724
725
// ---------------------------------------------------------------------------
726
727
0
bool gridshiftData::loadGridsIfNeeded(PJ *P) {
728
0
    if (m_error_code_in_defer_grid_opening) {
729
0
        proj_errno_set(P, m_error_code_in_defer_grid_opening);
730
0
        return false;
731
0
    } else if (m_defer_grid_opening) {
732
0
        m_defer_grid_opening = false;
733
0
        m_grids = pj_generic_grid_init(P, "grids");
734
0
        m_error_code_in_defer_grid_opening = proj_errno(P);
735
0
        if (m_error_code_in_defer_grid_opening) {
736
0
            return false;
737
0
        }
738
0
        bool isProjectedCoord;
739
0
        if (!checkGridTypes(P, isProjectedCoord)) {
740
0
            return false;
741
0
        }
742
0
    }
743
0
    return true;
744
0
}
745
746
// ---------------------------------------------------------------------------
747
748
// ---------------------------------------------------------------------------
749
750
static const std::string sHORIZONTAL_OFFSET("HORIZONTAL_OFFSET");
751
752
0
PJ_XYZ gridshiftData::apply(PJ *P, PJ_DIRECTION direction, PJ_XYZ xyz) {
753
0
    PJ_XYZ out;
754
755
0
    out.x = HUGE_VAL;
756
0
    out.y = HUGE_VAL;
757
0
    out.z = HUGE_VAL;
758
759
0
    std::string &type = m_mainGridType;
760
0
    bool bFoundGeog3DOffset = false;
761
0
    while (true) {
762
0
        GenericShiftGridSet *gridset = nullptr;
763
0
        const GenericShiftGrid *grid = findGrid(type, xyz, gridset);
764
0
        if (!grid) {
765
0
            if (m_mainGridTypeIsGeographic3DOffset && m_bHasHorizontalOffset) {
766
                // If we have a mix of grids with GEOGRAPHIC_3D_OFFSET
767
                // and HORIZONTAL_OFFSET+ELLIPSOIDAL_HEIGHT_OFFSET
768
0
                type = sHORIZONTAL_OFFSET;
769
0
                grid = findGrid(type, xyz, gridset);
770
0
            }
771
0
            if (!grid) {
772
0
                proj_context_errno_set(P->ctx,
773
0
                                       PROJ_ERR_COORD_TRANSFM_OUTSIDE_GRID);
774
0
                return out;
775
0
            }
776
0
        } else {
777
0
            if (m_mainGridTypeIsGeographic3DOffset)
778
0
                bFoundGeog3DOffset = true;
779
0
        }
780
781
0
        if (grid->isNullGrid()) {
782
0
            out = xyz;
783
0
            break;
784
0
        }
785
0
        bool shouldRetry = false;
786
0
        out = grid_apply_internal(
787
0
            P->ctx, type, !(m_bHasGeographic3DOffset || m_bHasHorizontalOffset),
788
0
            xyz, direction, grid, gridset, shouldRetry);
789
0
        if (!shouldRetry) {
790
0
            break;
791
0
        }
792
0
    }
793
794
0
    if (out.x == HUGE_VAL || out.y == HUGE_VAL) {
795
0
        if (proj_context_errno(P->ctx) == 0) {
796
0
            proj_context_errno_set(P->ctx, PROJ_ERR_COORD_TRANSFM_OUTSIDE_GRID);
797
0
        }
798
0
        return out;
799
0
    }
800
801
    // Second pass to apply vertical transformation, if it is in a
802
    // separate grid than lat-lon offsets.
803
0
    if (!bFoundGeog3DOffset && !m_auxGridType.empty()) {
804
0
        xyz = out;
805
0
        while (true) {
806
0
            GenericShiftGridSet *gridset = nullptr;
807
0
            const auto grid = findGrid(m_auxGridType, xyz, gridset);
808
0
            if (!grid) {
809
0
                proj_context_errno_set(P->ctx,
810
0
                                       PROJ_ERR_COORD_TRANSFM_OUTSIDE_GRID);
811
0
                return out;
812
0
            }
813
0
            if (grid->isNullGrid()) {
814
0
                break;
815
0
            }
816
817
0
            bool shouldRetry = false;
818
0
            out = grid_apply_internal(P->ctx, m_auxGridType, true, xyz,
819
0
                                      direction, grid, gridset, shouldRetry);
820
0
            if (!shouldRetry) {
821
0
                break;
822
0
            }
823
0
        }
824
825
0
        if (out.x == HUGE_VAL || out.y == HUGE_VAL) {
826
0
            proj_context_errno_set(P->ctx, PROJ_ERR_COORD_TRANSFM_OUTSIDE_GRID);
827
0
            return out;
828
0
        }
829
0
    }
830
831
0
    return out;
832
0
}
833
834
} // anonymous namespace
835
836
// ---------------------------------------------------------------------------
837
838
0
static PJ_XYZ pj_gridshift_forward_3d(PJ_LPZ lpz, PJ *P) {
839
0
    auto Q = static_cast<gridshiftData *>(P->opaque);
840
841
0
    if (!Q->loadGridsIfNeeded(P)) {
842
0
        return proj_coord_error().xyz;
843
0
    }
844
845
0
    PJ_XYZ xyz;
846
0
    xyz.x = lpz.lam;
847
0
    xyz.y = lpz.phi;
848
0
    xyz.z = lpz.z;
849
850
0
    xyz = Q->apply(P, PJ_FWD, xyz);
851
852
0
    xyz.x += Q->m_offsetX;
853
0
    xyz.y += Q->m_offsetY;
854
855
0
    return xyz;
856
0
}
857
858
// ---------------------------------------------------------------------------
859
860
0
static PJ_LPZ pj_gridshift_reverse_3d(PJ_XYZ xyz, PJ *P) {
861
0
    auto Q = static_cast<gridshiftData *>(P->opaque);
862
863
    // Must be done before using m_offsetX !
864
0
    if (!Q->loadGridsIfNeeded(P)) {
865
0
        return proj_coord_error().lpz;
866
0
    }
867
868
0
    xyz.x -= Q->m_offsetX;
869
0
    xyz.y -= Q->m_offsetY;
870
871
0
    PJ_XYZ xyz_out = Q->apply(P, PJ_INV, xyz);
872
0
    PJ_LPZ lpz;
873
0
    lpz.lam = xyz_out.x;
874
0
    lpz.phi = xyz_out.y;
875
0
    lpz.z = xyz_out.z;
876
0
    return lpz;
877
0
}
878
879
// ---------------------------------------------------------------------------
880
881
790
static PJ *pj_gridshift_destructor(PJ *P, int errlev) {
882
790
    if (nullptr == P)
883
0
        return nullptr;
884
885
790
    delete static_cast<struct gridshiftData *>(P->opaque);
886
790
    P->opaque = nullptr;
887
888
790
    return pj_default_destructor(P, errlev);
889
790
}
890
891
// ---------------------------------------------------------------------------
892
893
0
static void pj_gridshift_reassign_context(PJ *P, PJ_CONTEXT *ctx) {
894
0
    auto Q = (struct gridshiftData *)P->opaque;
895
0
    for (auto &grid : Q->m_grids) {
896
0
        grid->reassign_context(ctx);
897
0
    }
898
0
}
899
900
// ---------------------------------------------------------------------------
901
902
790
PJ *PJ_TRANSFORMATION(gridshift, 0) {
903
790
    auto Q = new gridshiftData;
904
790
    P->opaque = (void *)Q;
905
790
    P->destructor = pj_gridshift_destructor;
906
790
    P->reassign_context = pj_gridshift_reassign_context;
907
908
790
    P->fwd3d = pj_gridshift_forward_3d;
909
790
    P->inv3d = pj_gridshift_reverse_3d;
910
790
    P->fwd = nullptr;
911
790
    P->inv = nullptr;
912
913
790
    if (0 == pj_param(P->ctx, P->params, "tgrids").i) {
914
11
        proj_log_error(P, _("+grids parameter missing."));
915
11
        return pj_gridshift_destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
916
11
    }
917
918
779
    bool isKnownGrid = false;
919
779
    bool isProjectedCoord = false;
920
921
779
    if (!P->ctx->defer_grid_opening ||
922
779
        !pj_param(P->ctx, P->params, "tcoord_type").i) {
923
779
        const char *gridnames = pj_param(P->ctx, P->params, "sgrids").s;
924
779
        gMutex.lock();
925
779
        const auto iter = gKnownGrids.find(gridnames);
926
779
        isKnownGrid = iter != gKnownGrids.end();
927
779
        if (isKnownGrid) {
928
492
            Q->m_defer_grid_opening = true;
929
492
            isProjectedCoord = iter->second;
930
492
        }
931
779
        gMutex.unlock();
932
779
    }
933
934
779
    if (P->ctx->defer_grid_opening || isKnownGrid) {
935
492
        Q->m_defer_grid_opening = true;
936
492
    } else {
937
287
        const char *gridnames = pj_param(P->ctx, P->params, "sgrids").s;
938
287
        Q->m_grids = pj_generic_grid_init(P, "grids");
939
        /* Was gridlist compiled properly? */
940
287
        if (proj_errno(P)) {
941
118
            proj_log_error(P, _("could not find required grid(s)."));
942
118
            return pj_gridshift_destructor(
943
118
                P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
944
118
        }
945
169
        if (!Q->checkGridTypes(P, isProjectedCoord)) {
946
11
            return pj_gridshift_destructor(
947
11
                P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
948
11
        }
949
950
158
        gMutex.lock();
951
158
        gKnownGrids[gridnames] = isProjectedCoord;
952
158
        gMutex.unlock();
953
158
    }
954
955
650
    if (pj_param(P->ctx, P->params, "tinterpolation").i) {
956
0
        const char *interpolation =
957
0
            pj_param(P->ctx, P->params, "sinterpolation").s;
958
0
        if (strcmp(interpolation, "bilinear") == 0 ||
959
0
            strcmp(interpolation, "biquadratic") == 0) {
960
0
            Q->m_interpolation = interpolation;
961
0
        } else {
962
0
            proj_log_error(P, _("Unsupported value for +interpolation."));
963
0
            return pj_gridshift_destructor(
964
0
                P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
965
0
        }
966
0
    }
967
968
650
    if (pj_param(P->ctx, P->params, "tno_z_transform").i) {
969
142
        Q->m_skip_z_transform = true;
970
142
    }
971
972
    // +coord_type not advertised in documentation on purpose for now.
973
    // It is probably useless to do it, as the only potential use case of it
974
    // would be for PROJ itself when generating pipelines with deferred grid
975
    // opening.
976
650
    if (pj_param(P->ctx, P->params, "tcoord_type").i) {
977
        // Check the coordinate type (projected/geographic) from the explicit
978
        // +coord_type switch. This is mostly only useful in deferred grid
979
        // opening, otherwise we have figured it out above in checkGridTypes()
980
0
        const char *coord_type = pj_param(P->ctx, P->params, "scoord_type").s;
981
0
        if (coord_type) {
982
0
            if (strcmp(coord_type, "projected") == 0) {
983
0
                if (!P->ctx->defer_grid_opening && !isProjectedCoord) {
984
0
                    proj_log_error(P,
985
0
                                   _("+coord_type=projected specified, but the "
986
0
                                     "grid is known to not be projected"));
987
0
                    return pj_gridshift_destructor(
988
0
                        P, PROJ_ERR_INVALID_OP_MISSING_ARG);
989
0
                }
990
0
                isProjectedCoord = true;
991
0
            } else if (strcmp(coord_type, "geographic") == 0) {
992
0
                if (!P->ctx->defer_grid_opening && isProjectedCoord) {
993
0
                    proj_log_error(P, _("+coord_type=geographic specified, but "
994
0
                                        "the grid is known to be projected"));
995
0
                    return pj_gridshift_destructor(
996
0
                        P, PROJ_ERR_INVALID_OP_MISSING_ARG);
997
0
                }
998
0
            } else {
999
0
                proj_log_error(P, _("Unsupported value for +coord_type: valid "
1000
0
                                    "values are 'geographic' or 'projected'"));
1001
0
                return pj_gridshift_destructor(P,
1002
0
                                               PROJ_ERR_INVALID_OP_MISSING_ARG);
1003
0
            }
1004
0
        }
1005
0
    }
1006
1007
650
    if (isKnownGrid || pj_param(P->ctx, P->params, "tcoord_type").i) {
1008
492
        if (isProjectedCoord) {
1009
0
            P->left = PJ_IO_UNITS_PROJECTED;
1010
0
            P->right = PJ_IO_UNITS_PROJECTED;
1011
492
        } else {
1012
492
            P->left = PJ_IO_UNITS_RADIANS;
1013
492
            P->right = PJ_IO_UNITS_RADIANS;
1014
492
        }
1015
492
    } else {
1016
158
        P->left = PJ_IO_UNITS_WHATEVER;
1017
158
        P->right = PJ_IO_UNITS_WHATEVER;
1018
158
    }
1019
1020
650
    return P;
1021
650
}
1022
1023
// ---------------------------------------------------------------------------
1024
1025
0
void pj_clear_gridshift_knowngrids_cache() {
1026
0
    std::lock_guard<std::mutex> lock(gMutex);
1027
0
    gKnownGrids.clear();
1028
0
}