Coverage Report

Created: 2025-07-23 06:58

/src/PROJ/src/transformations/defmodel.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 * Project:  PROJ
3
 * Purpose:  Functionality related to deformation model
4
 * Author:   Even Rouault, <even.rouault at spatialys.com>
5
 *
6
 ******************************************************************************
7
 * Copyright (c) 2020, 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
#define PROJ_COMPILATION
29
30
#include "defmodel.hpp"
31
#include "filemanager.hpp"
32
#include "grids.hpp"
33
#include "proj_internal.h"
34
35
#include <assert.h>
36
37
#include <map>
38
#include <memory>
39
#include <utility>
40
41
PROJ_HEAD(defmodel, "Deformation model");
42
43
using namespace DeformationModel;
44
45
namespace {
46
47
struct Grid : public GridPrototype {
48
    PJ_CONTEXT *ctx;
49
    const NS_PROJ::GenericShiftGrid *realGrid;
50
    mutable bool checkedHorizontal = false;
51
    mutable bool checkedVertical = false;
52
    mutable int sampleX = 0;
53
    mutable int sampleY = 1;
54
    mutable int sampleZ = 2;
55
56
    Grid(PJ_CONTEXT *ctxIn, const NS_PROJ::GenericShiftGrid *realGridIn)
57
0
        : ctx(ctxIn), realGrid(realGridIn) {
58
0
        minx = realGridIn->extentAndRes().west;
59
0
        miny = realGridIn->extentAndRes().south;
60
0
        resx = realGridIn->extentAndRes().resX;
61
0
        resy = realGridIn->extentAndRes().resY;
62
0
        width = realGridIn->width();
63
0
        height = realGridIn->height();
64
0
    }
65
66
0
    bool checkHorizontal(const std::string &expectedUnit) const {
67
0
        if (!checkedHorizontal) {
68
0
            const auto samplesPerPixel = realGrid->samplesPerPixel();
69
0
            if (samplesPerPixel < 2) {
70
0
                pj_log(ctx, PJ_LOG_ERROR, "grid %s has not enough samples",
71
0
                       realGrid->name().c_str());
72
0
                return false;
73
0
            }
74
0
            bool foundDescX = false;
75
0
            bool foundDescY = false;
76
0
            bool foundDesc = false;
77
0
            for (int i = 0; i < samplesPerPixel; i++) {
78
0
                const auto desc = realGrid->description(i);
79
0
                if (desc == "east_offset") {
80
0
                    sampleX = i;
81
0
                    foundDescX = true;
82
0
                } else if (desc == "north_offset") {
83
0
                    sampleY = i;
84
0
                    foundDescY = true;
85
0
                }
86
0
                if (!desc.empty()) {
87
0
                    foundDesc = true;
88
0
                }
89
0
            }
90
0
            if (foundDesc && (!foundDescX || !foundDescY)) {
91
0
                pj_log(ctx, PJ_LOG_ERROR,
92
0
                       "grid %s : Found band description, "
93
0
                       "but not the ones expected",
94
0
                       realGrid->name().c_str());
95
0
                return false;
96
0
            }
97
0
            const auto unit = realGrid->unit(sampleX);
98
0
            if (!unit.empty() && unit != expectedUnit) {
99
0
                pj_log(ctx, PJ_LOG_ERROR,
100
0
                       "grid %s : Only unit=%s "
101
0
                       "currently handled for this mode",
102
0
                       realGrid->name().c_str(), expectedUnit.c_str());
103
0
                return false;
104
0
            }
105
0
            checkedHorizontal = true;
106
0
        }
107
0
        return true;
108
0
    }
109
110
    bool getLongLatOffset(int ix, int iy, double &longOffsetRadian,
111
0
                          double &latOffsetRadian) const {
112
0
        if (!checkHorizontal(STR_DEGREE)) {
113
0
            return false;
114
0
        }
115
0
        float longOffsetDeg;
116
0
        float latOffsetDeg;
117
0
        if (!realGrid->valueAt(ix, iy, sampleX, longOffsetDeg) ||
118
0
            !realGrid->valueAt(ix, iy, sampleY, latOffsetDeg)) {
119
0
            return false;
120
0
        }
121
0
        longOffsetRadian = longOffsetDeg * DEG_TO_RAD;
122
0
        latOffsetRadian = latOffsetDeg * DEG_TO_RAD;
123
0
        return true;
124
0
    }
125
126
0
    bool getZOffset(int ix, int iy, double &zOffset) const {
127
0
        if (!checkedVertical) {
128
0
            const auto samplesPerPixel = realGrid->samplesPerPixel();
129
0
            if (samplesPerPixel == 1) {
130
0
                sampleZ = 0;
131
0
            } else if (samplesPerPixel < 3) {
132
0
                pj_log(ctx, PJ_LOG_ERROR, "grid %s has not enough samples",
133
0
                       realGrid->name().c_str());
134
0
                return false;
135
0
            }
136
0
            bool foundDesc = false;
137
0
            bool foundDescZ = false;
138
0
            for (int i = 0; i < samplesPerPixel; i++) {
139
0
                const auto desc = realGrid->description(i);
140
0
                if (desc == "vertical_offset") {
141
0
                    sampleZ = i;
142
0
                    foundDescZ = true;
143
0
                }
144
0
                if (!desc.empty()) {
145
0
                    foundDesc = true;
146
0
                }
147
0
            }
148
0
            if (foundDesc && !foundDescZ) {
149
0
                pj_log(ctx, PJ_LOG_ERROR,
150
0
                       "grid %s : Found band description, "
151
0
                       "but not the ones expected",
152
0
                       realGrid->name().c_str());
153
0
                return false;
154
0
            }
155
0
            const auto unit = realGrid->unit(sampleZ);
156
0
            if (!unit.empty() && unit != STR_METRE) {
157
0
                pj_log(ctx, PJ_LOG_ERROR,
158
0
                       "grid %s : Only unit=metre currently "
159
0
                       "handled for this mode",
160
0
                       realGrid->name().c_str());
161
0
                return false;
162
0
            }
163
0
            checkedVertical = true;
164
0
        }
165
0
        float zOffsetFloat = 0.0f;
166
0
        const bool ret = realGrid->valueAt(ix, iy, sampleZ, zOffsetFloat);
167
0
        zOffset = zOffsetFloat;
168
0
        return ret;
169
0
    }
170
171
    bool getEastingNorthingOffset(int ix, int iy, double &eastingOffset,
172
0
                                  double &northingOffset) const {
173
0
        if (!checkHorizontal(STR_METRE)) {
174
0
            return false;
175
0
        }
176
0
        float eastingOffsetFloat = 0.0f;
177
0
        float northingOffsetFloat = 0.0f;
178
0
        const bool ret =
179
0
            realGrid->valueAt(ix, iy, sampleX, eastingOffsetFloat) &&
180
0
            realGrid->valueAt(ix, iy, sampleY, northingOffsetFloat);
181
0
        eastingOffset = eastingOffsetFloat;
182
0
        northingOffset = northingOffsetFloat;
183
0
        return ret;
184
0
    }
185
186
    bool getLongLatZOffset(int ix, int iy, double &longOffsetRadian,
187
0
                           double &latOffsetRadian, double &zOffset) const {
188
0
        return getLongLatOffset(ix, iy, longOffsetRadian, latOffsetRadian) &&
189
0
               getZOffset(ix, iy, zOffset);
190
0
    }
191
192
    bool getEastingNorthingZOffset(int ix, int iy, double &eastingOffset,
193
                                   double &northingOffset,
194
0
                                   double &zOffset) const {
195
0
        return getEastingNorthingOffset(ix, iy, eastingOffset,
196
0
                                        northingOffset) &&
197
0
               getZOffset(ix, iy, zOffset);
198
0
    }
199
200
#ifdef DEBUG_DEFMODEL
201
    std::string name() const { return realGrid->name(); }
202
#endif
203
204
  private:
205
    Grid(const Grid &) = delete;
206
    Grid &operator=(const Grid &) = delete;
207
};
208
209
struct GridSet : public GridSetPrototype<Grid> {
210
211
    PJ_CONTEXT *ctx;
212
    std::unique_ptr<NS_PROJ::GenericShiftGridSet> realGridSet;
213
    std::map<const NS_PROJ::GenericShiftGrid *, std::unique_ptr<Grid>>
214
        mapGrids{};
215
216
    GridSet(PJ_CONTEXT *ctxIn,
217
            std::unique_ptr<NS_PROJ::GenericShiftGridSet> &&realGridSetIn)
218
0
        : ctx(ctxIn), realGridSet(std::move(realGridSetIn)) {}
219
220
0
    const Grid *gridAt(double x, double y) {
221
0
        const NS_PROJ::GenericShiftGrid *realGrid = realGridSet->gridAt(x, y);
222
0
        if (!realGrid) {
223
0
            return nullptr;
224
0
        }
225
0
        auto iter = mapGrids.find(realGrid);
226
0
        if (iter == mapGrids.end()) {
227
0
            iter = mapGrids
228
0
                       .insert(std::pair<const NS_PROJ::GenericShiftGrid *,
229
0
                                         std::unique_ptr<Grid>>(
230
0
                           realGrid,
231
0
                           std::unique_ptr<Grid>(new Grid(ctx, realGrid))))
232
0
                       .first;
233
0
        }
234
0
        return iter->second.get();
235
0
    }
236
237
  private:
238
    GridSet(const GridSet &) = delete;
239
    GridSet &operator=(const GridSet &) = delete;
240
};
241
242
struct EvaluatorIface : public EvaluatorIfacePrototype<Grid, GridSet> {
243
244
    PJ_CONTEXT *ctx;
245
    PJ *cart;
246
247
9
    EvaluatorIface(PJ_CONTEXT *ctxIn, PJ *cartIn) : ctx(ctxIn), cart(cartIn) {}
248
249
9
    ~EvaluatorIface() {
250
9
        if (cart)
251
9
            cart->destructor(cart, 0);
252
9
    }
253
254
0
    std::unique_ptr<GridSet> open(const std::string &filename) {
255
0
        auto realGridSet = NS_PROJ::GenericShiftGridSet::open(ctx, filename);
256
0
        if (!realGridSet) {
257
0
            pj_log(ctx, PJ_LOG_ERROR, "cannot open %s", filename.c_str());
258
0
            return nullptr;
259
0
        }
260
0
        return std::unique_ptr<GridSet>(
261
0
            new GridSet(ctx, std::move(realGridSet)));
262
0
    }
263
264
0
    bool isGeographicCRS(const std::string &crsDef) {
265
0
        PJ *P = proj_create(ctx, crsDef.c_str());
266
0
        if (P == nullptr) {
267
0
            return true; // reasonable default value
268
0
        }
269
0
        const auto type = proj_get_type(P);
270
0
        bool ret = (type == PJ_TYPE_GEOGRAPHIC_2D_CRS ||
271
0
                    type == PJ_TYPE_GEOGRAPHIC_3D_CRS);
272
0
        proj_destroy(P);
273
0
        return ret;
274
0
    }
275
276
    void geographicToGeocentric(double lam, double phi, double height, double a,
277
                                double b, double /*es*/, double &X, double &Y,
278
0
                                double &Z) {
279
0
        (void)a;
280
0
        (void)b;
281
0
        assert(cart->a == a);
282
0
        assert(cart->b == b);
283
0
        PJ_LPZ lpz;
284
0
        lpz.lam = lam;
285
0
        lpz.phi = phi;
286
0
        lpz.z = height;
287
0
        PJ_XYZ xyz = cart->fwd3d(lpz, cart);
288
0
        X = xyz.x;
289
0
        Y = xyz.y;
290
0
        Z = xyz.z;
291
0
    }
292
293
    void geocentricToGeographic(double X, double Y, double Z, double a,
294
                                double b, double /*es*/, double &lam,
295
0
                                double &phi, double &height) {
296
0
        (void)a;
297
0
        (void)b;
298
0
        assert(cart->a == a);
299
0
        assert(cart->b == b);
300
0
        PJ_XYZ xyz;
301
0
        xyz.x = X;
302
0
        xyz.y = Y;
303
0
        xyz.z = Z;
304
0
        PJ_LPZ lpz = cart->inv3d(xyz, cart);
305
0
        lam = lpz.lam;
306
0
        phi = lpz.phi;
307
0
        height = lpz.z;
308
0
    }
309
310
#ifdef DEBUG_DEFMODEL
311
    void log(const std::string &msg) {
312
        pj_log(ctx, PJ_LOG_TRACE, "%s", msg.c_str());
313
    }
314
#endif
315
316
  private:
317
    EvaluatorIface(const EvaluatorIface &) = delete;
318
    EvaluatorIface &operator=(const EvaluatorIface &) = delete;
319
};
320
321
struct defmodelData {
322
    std::unique_ptr<Evaluator<Grid, GridSet, EvaluatorIface>> evaluator{};
323
    EvaluatorIface evaluatorIface;
324
325
    explicit defmodelData(PJ_CONTEXT *ctx, PJ *cart)
326
9
        : evaluatorIface(ctx, cart) {}
327
328
    defmodelData(const defmodelData &) = delete;
329
    defmodelData &operator=(const defmodelData &) = delete;
330
};
331
332
} // namespace
333
334
9
static PJ *destructor(PJ *P, int errlev) {
335
9
    if (nullptr == P)
336
0
        return nullptr;
337
338
9
    auto Q = static_cast<struct defmodelData *>(P->opaque);
339
9
    delete Q;
340
9
    P->opaque = nullptr;
341
342
9
    return pj_default_destructor(P, errlev);
343
9
}
344
345
0
static void forward_4d(PJ_COORD &coo, PJ *P) {
346
0
    auto *Q = (struct defmodelData *)P->opaque;
347
348
0
    if (coo.xyzt.t == HUGE_VAL) {
349
0
        coo = proj_coord_error();
350
0
        proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_MISSING_TIME);
351
0
        return;
352
0
    }
353
354
0
    if (!Q->evaluator->forward(Q->evaluatorIface, coo.xyzt.x, coo.xyzt.y,
355
0
                               coo.xyzt.z, coo.xyzt.t, coo.xyzt.x, coo.xyzt.y,
356
0
                               coo.xyzt.z)) {
357
0
        coo = proj_coord_error();
358
0
    }
359
0
}
360
361
0
static void reverse_4d(PJ_COORD &coo, PJ *P) {
362
0
    auto *Q = (struct defmodelData *)P->opaque;
363
364
0
    if (coo.xyzt.t == HUGE_VAL) {
365
0
        coo = proj_coord_error();
366
0
        proj_errno_set(P, PROJ_ERR_COORD_TRANSFM_MISSING_TIME);
367
0
        return;
368
0
    }
369
370
0
    if (!Q->evaluator->inverse(Q->evaluatorIface, coo.xyzt.x, coo.xyzt.y,
371
0
                               coo.xyzt.z, coo.xyzt.t, coo.xyzt.x, coo.xyzt.y,
372
0
                               coo.xyzt.z)) {
373
0
        coo = proj_coord_error();
374
0
    }
375
0
}
376
377
// Function called by proj_assign_context() when a new context is assigned to
378
// an existing PJ object. Mostly to deal with objects being passed between
379
// threads.
380
0
static void reassign_context(PJ *P, PJ_CONTEXT *ctx) {
381
0
    auto *Q = (struct defmodelData *)P->opaque;
382
0
    if (Q->evaluatorIface.ctx != ctx) {
383
0
        Q->evaluator->clearGridCache();
384
0
        Q->evaluatorIface.ctx = ctx;
385
0
    }
386
0
}
387
388
9
PJ *PJ_TRANSFORMATION(defmodel, 1) {
389
    // Pass a dummy ellipsoid definition that will be overridden just afterwards
390
9
    auto cart = proj_create(P->ctx, "+proj=cart +a=1");
391
9
    if (cart == nullptr)
392
0
        return destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
393
394
    /* inherit ellipsoid definition from P to Q->cart */
395
9
    pj_inherit_ellipsoid_def(P, cart);
396
397
9
    auto Q = new defmodelData(P->ctx, cart);
398
9
    P->opaque = (void *)Q;
399
9
    P->destructor = destructor;
400
9
    P->reassign_context = reassign_context;
401
402
9
    const char *model = pj_param(P->ctx, P->params, "smodel").s;
403
9
    if (!model) {
404
3
        proj_log_error(P, _("+model= should be specified."));
405
3
        return destructor(P, PROJ_ERR_INVALID_OP_MISSING_ARG);
406
3
    }
407
408
6
    auto file = NS_PROJ::FileManager::open_resource_file(P->ctx, model);
409
6
    if (nullptr == file) {
410
3
        proj_log_error(P, _("Cannot open %s"), model);
411
3
        return destructor(P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
412
3
    }
413
3
    file->seek(0, SEEK_END);
414
3
    unsigned long long size = file->tell();
415
    // Arbitrary threshold to avoid ingesting an arbitrarily large JSON file,
416
    // that could be a denial of service risk. 10 MB should be sufficiently
417
    // large for any valid use !
418
3
    if (size > 10 * 1024 * 1024) {
419
0
        proj_log_error(P, _("File %s too large"), model);
420
0
        return destructor(P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
421
0
    }
422
3
    file->seek(0);
423
3
    std::string jsonStr;
424
3
    jsonStr.resize(static_cast<size_t>(size));
425
3
    if (file->read(&jsonStr[0], jsonStr.size()) != jsonStr.size()) {
426
0
        proj_log_error(P, _("Cannot read %s"), model);
427
0
        return destructor(P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
428
0
    }
429
430
3
    try {
431
3
        Q->evaluator.reset(new Evaluator<Grid, GridSet, EvaluatorIface>(
432
3
            MasterFile::parse(jsonStr), Q->evaluatorIface, P->a, P->b));
433
3
    } catch (const std::exception &e) {
434
3
        proj_log_error(P, _("invalid model: %s"), e.what());
435
3
        return destructor(P, PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID);
436
3
    }
437
438
0
    P->fwd4d = forward_4d;
439
0
    P->inv4d = reverse_4d;
440
441
0
    if (Q->evaluator->isGeographicCRS()) {
442
0
        P->left = PJ_IO_UNITS_RADIANS;
443
0
        P->right = PJ_IO_UNITS_RADIANS;
444
0
    } else {
445
0
        P->left = PJ_IO_UNITS_PROJECTED;
446
0
        P->right = PJ_IO_UNITS_PROJECTED;
447
0
    }
448
449
0
    return P;
450
3
}