Coverage Report

Created: 2025-07-23 06:58

/src/PROJ/src/crs_to_crs.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 * Project:  PROJ
3
 * Purpose:  Implements proj_create_crs_to_crs() and the like
4
 *
5
 * Author:   Even Rouault, <even.rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2018-2024, Even Rouault, <even.rouault at spatialys.com>
9
 *
10
 * Permission is hereby granted, free of charge, to any person obtaining a
11
 * copy of this software and associated documentation files (the "Software"),
12
 * to deal in the Software without restriction, including without limitation
13
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
14
 * and/or sell copies of the Software, and to permit persons to whom the
15
 * Software is furnished to do so, subject to the following conditions:
16
 *
17
 * The above copyright notice and this permission notice shall be included
18
 * in all copies or substantial portions of the Software.
19
 *
20
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26
 * DEALINGS IN THE SOFTWARE.
27
 *****************************************************************************/
28
29
#define FROM_PROJ_CPP
30
31
#include "proj.h"
32
#include "proj_internal.h"
33
#include <math.h>
34
35
#include <algorithm>
36
#include <limits>
37
38
#include "proj/internal/internal.hpp"
39
40
using namespace NS_PROJ::internal;
41
42
/** Adds a " +type=crs" suffix to a PROJ string (if it is a PROJ string) */
43
25.5k
std::string pj_add_type_crs_if_needed(const std::string &str) {
44
25.5k
    std::string ret(str);
45
25.5k
    if ((starts_with(str, "proj=") || starts_with(str, "+proj=") ||
46
25.5k
         starts_with(str, "+init=") || starts_with(str, "+title=")) &&
47
25.5k
        str.find("type=crs") == std::string::npos) {
48
9.49k
        ret += " +type=crs";
49
9.49k
    }
50
25.5k
    return ret;
51
25.5k
}
52
53
/*****************************************************************************/
54
static void reproject_bbox(PJ *pjGeogToCrs, double west_lon, double south_lat,
55
                           double east_lon, double north_lat, double &minx,
56
34.7k
                           double &miny, double &maxx, double &maxy) {
57
    /*****************************************************************************/
58
59
34.7k
    minx = -std::numeric_limits<double>::max();
60
34.7k
    miny = -std::numeric_limits<double>::max();
61
34.7k
    maxx = std::numeric_limits<double>::max();
62
34.7k
    maxy = std::numeric_limits<double>::max();
63
64
34.7k
    if (!(west_lon == -180.0 && east_lon == 180.0 && south_lat == -90.0 &&
65
34.7k
          north_lat == 90.0)) {
66
33.0k
        minx = -minx;
67
33.0k
        miny = -miny;
68
33.0k
        maxx = -maxx;
69
33.0k
        maxy = -maxy;
70
71
33.0k
        constexpr int N_STEPS = 20;
72
33.0k
        constexpr int N_STEPS_P1 = N_STEPS + 1;
73
33.0k
        constexpr int XY_SIZE = N_STEPS_P1 * 4;
74
33.0k
        std::vector<double> x(XY_SIZE);
75
33.0k
        std::vector<double> y(XY_SIZE);
76
33.0k
        const double step_lon = (east_lon - west_lon) / N_STEPS;
77
33.0k
        const double step_lat = (north_lat - south_lat) / N_STEPS;
78
727k
        for (int j = 0; j <= N_STEPS; j++) {
79
694k
            x[j] = west_lon + j * step_lon;
80
694k
            y[j] = south_lat;
81
694k
            x[N_STEPS_P1 + j] = x[j];
82
694k
            y[N_STEPS_P1 + j] = north_lat;
83
694k
            x[N_STEPS_P1 * 2 + j] = west_lon;
84
694k
            y[N_STEPS_P1 * 2 + j] = south_lat + j * step_lat;
85
694k
            x[N_STEPS_P1 * 3 + j] = east_lon;
86
694k
            y[N_STEPS_P1 * 3 + j] = y[N_STEPS_P1 * 2 + j];
87
694k
        }
88
33.0k
        proj_trans_generic(pjGeogToCrs, PJ_FWD, &x[0], sizeof(double), XY_SIZE,
89
33.0k
                           &y[0], sizeof(double), XY_SIZE, nullptr, 0, 0,
90
33.0k
                           nullptr, 0, 0);
91
2.80M
        for (int j = 0; j < XY_SIZE; j++) {
92
2.77M
            if (x[j] != HUGE_VAL && y[j] != HUGE_VAL) {
93
1.94M
                minx = std::min(minx, x[j]);
94
1.94M
                miny = std::min(miny, y[j]);
95
1.94M
                maxx = std::max(maxx, x[j]);
96
1.94M
                maxy = std::max(maxy, y[j]);
97
1.94M
            }
98
2.77M
        }
99
33.0k
    }
100
34.7k
}
101
102
/*****************************************************************************/
103
static PJ *add_coord_op_to_list(
104
    int idxInOriginalList, PJ *op, double west_lon, double south_lat,
105
    double east_lon, double north_lat, PJ *pjGeogToSrc, PJ *pjGeogToDst,
106
    const PJ *pjSrcGeocentricToLonLat, const PJ *pjDstGeocentricToLonLat,
107
19.5k
    const char *areaName, std::vector<PJCoordOperation> &altCoordOps) {
108
    /*****************************************************************************/
109
110
19.5k
    double minxSrc;
111
19.5k
    double minySrc;
112
19.5k
    double maxxSrc;
113
19.5k
    double maxySrc;
114
19.5k
    double minxDst;
115
19.5k
    double minyDst;
116
19.5k
    double maxxDst;
117
19.5k
    double maxyDst;
118
119
19.5k
    double w = west_lon / 180 * M_PI;
120
19.5k
    double s = south_lat / 180 * M_PI;
121
19.5k
    double e = east_lon / 180 * M_PI;
122
19.5k
    double n = north_lat / 180 * M_PI;
123
19.5k
    if (w > e) {
124
0
        e += 2 * M_PI;
125
0
    }
126
    // Integrate cos(lat) between south_lat and north_lat
127
19.5k
    const double pseudoArea = (e - w) * (std::sin(n) - std::sin(s));
128
129
19.5k
    if (pjSrcGeocentricToLonLat) {
130
4.13k
        minxSrc = west_lon;
131
4.13k
        minySrc = south_lat;
132
4.13k
        maxxSrc = east_lon;
133
4.13k
        maxySrc = north_lat;
134
15.3k
    } else {
135
15.3k
        reproject_bbox(pjGeogToSrc, west_lon, south_lat, east_lon, north_lat,
136
15.3k
                       minxSrc, minySrc, maxxSrc, maxySrc);
137
15.3k
    }
138
139
19.5k
    if (pjDstGeocentricToLonLat) {
140
163
        minxDst = west_lon;
141
163
        minyDst = south_lat;
142
163
        maxxDst = east_lon;
143
163
        maxyDst = north_lat;
144
19.3k
    } else {
145
19.3k
        reproject_bbox(pjGeogToDst, west_lon, south_lat, east_lon, north_lat,
146
19.3k
                       minxDst, minyDst, maxxDst, maxyDst);
147
19.3k
    }
148
149
19.5k
    if (minxSrc <= maxxSrc && minxDst <= maxxDst) {
150
10.6k
        const char *c_name = proj_get_name(op);
151
10.6k
        std::string name(c_name ? c_name : "");
152
153
10.6k
        const double accuracy = proj_coordoperation_get_accuracy(op->ctx, op);
154
10.6k
        altCoordOps.emplace_back(
155
10.6k
            idxInOriginalList, minxSrc, minySrc, maxxSrc, maxySrc, minxDst,
156
10.6k
            minyDst, maxxDst, maxyDst, op, name, accuracy, pseudoArea, areaName,
157
10.6k
            pjSrcGeocentricToLonLat, pjDstGeocentricToLonLat);
158
10.6k
        op = nullptr;
159
10.6k
    }
160
19.5k
    return op;
161
19.5k
}
162
163
namespace {
164
struct ObjectKeeper {
165
    PJ *m_obj = nullptr;
166
8
    explicit ObjectKeeper(PJ *obj) : m_obj(obj) {}
167
8
    ~ObjectKeeper() { proj_destroy(m_obj); }
168
    ObjectKeeper(const ObjectKeeper &) = delete;
169
    ObjectKeeper &operator=(const ObjectKeeper &) = delete;
170
};
171
} // namespace
172
173
/*****************************************************************************/
174
1.77k
static PJ *create_operation_to_geog_crs(PJ_CONTEXT *ctx, const PJ *crs) {
175
    /*****************************************************************************/
176
177
1.77k
    std::unique_ptr<ObjectKeeper> keeper;
178
1.77k
    if (proj_get_type(crs) == PJ_TYPE_COORDINATE_METADATA) {
179
8
        auto tmp = proj_get_source_crs(ctx, crs);
180
8
        assert(tmp);
181
8
        keeper.reset(new ObjectKeeper(tmp));
182
8
        crs = tmp;
183
8
    }
184
1.77k
    (void)keeper;
185
186
    // Create a geographic 2D long-lat degrees CRS that is related to the
187
    // CRS
188
1.77k
    auto geodetic_crs = proj_crs_get_geodetic_crs(ctx, crs);
189
1.77k
    if (!geodetic_crs) {
190
0
        proj_context_log_debug(ctx, "Cannot find geodetic CRS matching CRS");
191
0
        return nullptr;
192
0
    }
193
194
1.77k
    auto geodetic_crs_type = proj_get_type(geodetic_crs);
195
1.77k
    if (geodetic_crs_type == PJ_TYPE_GEOCENTRIC_CRS ||
196
1.77k
        geodetic_crs_type == PJ_TYPE_GEOGRAPHIC_2D_CRS ||
197
1.77k
        geodetic_crs_type == PJ_TYPE_GEOGRAPHIC_3D_CRS) {
198
1.75k
        auto datum = proj_crs_get_datum_forced(ctx, geodetic_crs);
199
1.75k
        assert(datum);
200
1.75k
        auto cs = proj_create_ellipsoidal_2D_cs(
201
1.75k
            ctx, PJ_ELLPS2D_LONGITUDE_LATITUDE, nullptr, 0);
202
1.75k
        auto ellps = proj_get_ellipsoid(ctx, datum);
203
1.75k
        proj_destroy(datum);
204
1.75k
        double semi_major_metre = 0;
205
1.75k
        double inv_flattening = 0;
206
1.75k
        proj_ellipsoid_get_parameters(ctx, ellps, &semi_major_metre, nullptr,
207
1.75k
                                      nullptr, &inv_flattening);
208
        // It is critical to set the prime meridian to 0
209
1.75k
        auto temp = proj_create_geographic_crs(
210
1.75k
            ctx, "unnamed crs", "unnamed datum", proj_get_name(ellps),
211
1.75k
            semi_major_metre, inv_flattening, "Reference prime meridian", 0,
212
1.75k
            nullptr, 0, cs);
213
1.75k
        proj_destroy(ellps);
214
1.75k
        proj_destroy(cs);
215
1.75k
        proj_destroy(geodetic_crs);
216
1.75k
        geodetic_crs = temp;
217
1.75k
        geodetic_crs_type = proj_get_type(geodetic_crs);
218
1.75k
    }
219
1.77k
    if (geodetic_crs_type != PJ_TYPE_GEOGRAPHIC_2D_CRS) {
220
        // Shouldn't happen
221
19
        proj_context_log_debug(ctx, "Cannot find geographic CRS matching CRS");
222
19
        proj_destroy(geodetic_crs);
223
19
        return nullptr;
224
19
    }
225
226
    // Create the transformation from this geographic 2D CRS to the source CRS
227
1.75k
    auto operation_ctx = proj_create_operation_factory_context(ctx, nullptr);
228
1.75k
    proj_operation_factory_context_set_spatial_criterion(
229
1.75k
        ctx, operation_ctx, PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION);
230
1.75k
    proj_operation_factory_context_set_grid_availability_use(
231
1.75k
        ctx, operation_ctx,
232
1.75k
        PROJ_GRID_AVAILABILITY_DISCARD_OPERATION_IF_MISSING_GRID);
233
1.75k
    auto target_crs_2D = proj_crs_demote_to_2D(ctx, nullptr, crs);
234
1.75k
    auto op_list_to_geodetic =
235
1.75k
        proj_create_operations(ctx, geodetic_crs, target_crs_2D, operation_ctx);
236
1.75k
    proj_destroy(target_crs_2D);
237
1.75k
    proj_operation_factory_context_destroy(operation_ctx);
238
1.75k
    proj_destroy(geodetic_crs);
239
240
1.75k
    const int nOpCount = op_list_to_geodetic == nullptr
241
1.75k
                             ? 0
242
1.75k
                             : proj_list_get_count(op_list_to_geodetic);
243
1.75k
    if (nOpCount == 0) {
244
12
        proj_context_log_debug(
245
12
            ctx, "Cannot compute transformation from geographic CRS to CRS");
246
12
        proj_list_destroy(op_list_to_geodetic);
247
12
        return nullptr;
248
12
    }
249
1.74k
    PJ *opGeogToCrs = nullptr;
250
    // Use in priority operations *without* grids
251
1.77k
    for (int i = 0; i < nOpCount; i++) {
252
1.74k
        auto op = proj_list_get(ctx, op_list_to_geodetic, i);
253
1.74k
        assert(op);
254
1.74k
        if (proj_coordoperation_get_grid_used_count(ctx, op) == 0) {
255
1.72k
            opGeogToCrs = op;
256
1.72k
            break;
257
1.72k
        }
258
23
        proj_destroy(op);
259
23
    }
260
1.74k
    if (opGeogToCrs == nullptr) {
261
23
        opGeogToCrs = proj_list_get(ctx, op_list_to_geodetic, 0);
262
23
        assert(opGeogToCrs);
263
23
    }
264
1.74k
    proj_list_destroy(op_list_to_geodetic);
265
1.74k
    return opGeogToCrs;
266
1.75k
}
267
268
/*****************************************************************************/
269
static PJ *create_operation_geocentric_crs_to_geog_crs(PJ_CONTEXT *ctx,
270
                                                       const PJ *geocentric_crs)
271
/*****************************************************************************/
272
281
{
273
281
    assert(proj_get_type(geocentric_crs) == PJ_TYPE_GEOCENTRIC_CRS);
274
275
281
    auto datum = proj_crs_get_datum_forced(ctx, geocentric_crs);
276
281
    assert(datum);
277
281
    auto cs = proj_create_ellipsoidal_2D_cs(ctx, PJ_ELLPS2D_LONGITUDE_LATITUDE,
278
281
                                            nullptr, 0);
279
281
    auto ellps = proj_get_ellipsoid(ctx, datum);
280
281
    proj_destroy(datum);
281
281
    double semi_major_metre = 0;
282
281
    double inv_flattening = 0;
283
281
    proj_ellipsoid_get_parameters(ctx, ellps, &semi_major_metre, nullptr,
284
281
                                  nullptr, &inv_flattening);
285
    // It is critical to set the prime meridian to 0
286
281
    auto lon_lat_crs = proj_create_geographic_crs(
287
281
        ctx, "unnamed crs", "unnamed datum", proj_get_name(ellps),
288
281
        semi_major_metre, inv_flattening, "Reference prime meridian", 0,
289
281
        nullptr, 0, cs);
290
281
    proj_destroy(ellps);
291
281
    proj_destroy(cs);
292
293
    // Create the transformation from this geocentric CRS to the lon-lat one
294
281
    auto operation_ctx = proj_create_operation_factory_context(ctx, nullptr);
295
281
    proj_operation_factory_context_set_spatial_criterion(
296
281
        ctx, operation_ctx, PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION);
297
281
    proj_operation_factory_context_set_grid_availability_use(
298
281
        ctx, operation_ctx,
299
281
        PROJ_GRID_AVAILABILITY_DISCARD_OPERATION_IF_MISSING_GRID);
300
281
    auto op_list =
301
281
        proj_create_operations(ctx, geocentric_crs, lon_lat_crs, operation_ctx);
302
281
    proj_operation_factory_context_destroy(operation_ctx);
303
281
    proj_destroy(lon_lat_crs);
304
305
281
    const int nOpCount = op_list == nullptr ? 0 : proj_list_get_count(op_list);
306
281
    if (nOpCount != 1) {
307
0
        proj_context_log_debug(ctx, "Cannot compute transformation from "
308
0
                                    "geocentric CRS to geographic CRS");
309
0
        proj_list_destroy(op_list);
310
0
        return nullptr;
311
0
    }
312
313
281
    auto op = proj_list_get(ctx, op_list, 0);
314
281
    assert(op);
315
281
    proj_list_destroy(op_list);
316
281
    return op;
317
281
}
318
319
/*****************************************************************************/
320
PJ *proj_create_crs_to_crs(PJ_CONTEXT *ctx, const char *source_crs,
321
12.6k
                           const char *target_crs, PJ_AREA *area) {
322
    /******************************************************************************
323
        Create a transformation pipeline between two known coordinate reference
324
        systems.
325
326
        See docs/source/development/reference/functions.rst
327
328
    ******************************************************************************/
329
12.6k
    if (!ctx) {
330
12.6k
        ctx = pj_get_default_ctx();
331
12.6k
    }
332
333
12.6k
    PJ *src;
334
12.6k
    PJ *dst;
335
12.6k
    try {
336
12.6k
        std::string source_crs_modified(pj_add_type_crs_if_needed(source_crs));
337
12.6k
        std::string target_crs_modified(pj_add_type_crs_if_needed(target_crs));
338
339
12.6k
        src = proj_create(ctx, source_crs_modified.c_str());
340
12.6k
        if (!src) {
341
4.79k
            proj_context_log_debug(ctx, "Cannot instantiate source_crs");
342
4.79k
            return nullptr;
343
4.79k
        }
344
345
7.81k
        dst = proj_create(ctx, target_crs_modified.c_str());
346
7.81k
        if (!dst) {
347
2.69k
            proj_context_log_debug(ctx, "Cannot instantiate target_crs");
348
2.69k
            proj_destroy(src);
349
2.69k
            return nullptr;
350
2.69k
        }
351
7.81k
    } catch (const std::exception &) {
352
0
        return nullptr;
353
0
    }
354
355
5.12k
    auto ret = proj_create_crs_to_crs_from_pj(ctx, src, dst, area, nullptr);
356
5.12k
    proj_destroy(src);
357
5.12k
    proj_destroy(dst);
358
5.12k
    return ret;
359
12.6k
}
360
361
/*****************************************************************************/
362
std::vector<PJCoordOperation>
363
pj_create_prepared_operations(PJ_CONTEXT *ctx, const PJ *source_crs,
364
                              const PJ *target_crs, PJ_OBJ_LIST *op_list)
365
/*****************************************************************************/
366
1.03k
{
367
1.03k
    PJ *pjGeogToSrc = nullptr;
368
1.03k
    PJ *pjSrcGeocentricToLonLat = nullptr;
369
1.03k
    if (proj_get_type(source_crs) == PJ_TYPE_GEOCENTRIC_CRS) {
370
257
        pjSrcGeocentricToLonLat =
371
257
            create_operation_geocentric_crs_to_geog_crs(ctx, source_crs);
372
257
        if (!pjSrcGeocentricToLonLat) {
373
            // shouldn't happen
374
0
            return {};
375
0
        }
376
780
    } else {
377
780
        pjGeogToSrc = create_operation_to_geog_crs(ctx, source_crs);
378
780
        if (!pjGeogToSrc) {
379
15
            proj_context_log_debug(
380
15
                ctx, "Cannot create transformation from geographic "
381
15
                     "CRS of source CRS to source CRS");
382
15
            return {};
383
15
        }
384
780
    }
385
386
1.02k
    PJ *pjGeogToDst = nullptr;
387
1.02k
    PJ *pjDstGeocentricToLonLat = nullptr;
388
1.02k
    if (proj_get_type(target_crs) == PJ_TYPE_GEOCENTRIC_CRS) {
389
24
        pjDstGeocentricToLonLat =
390
24
            create_operation_geocentric_crs_to_geog_crs(ctx, target_crs);
391
24
        if (!pjDstGeocentricToLonLat) {
392
            // shouldn't happen
393
0
            proj_destroy(pjSrcGeocentricToLonLat);
394
0
            proj_destroy(pjGeogToSrc);
395
0
            return {};
396
0
        }
397
998
    } else {
398
998
        pjGeogToDst = create_operation_to_geog_crs(ctx, target_crs);
399
998
        if (!pjGeogToDst) {
400
16
            proj_context_log_debug(
401
16
                ctx, "Cannot create transformation from geographic "
402
16
                     "CRS of target CRS to target CRS");
403
16
            proj_destroy(pjSrcGeocentricToLonLat);
404
16
            proj_destroy(pjGeogToSrc);
405
16
            return {};
406
16
        }
407
998
    }
408
409
1.00k
    try {
410
1.00k
        std::vector<PJCoordOperation> preparedOpList;
411
412
        // Iterate over source->target candidate transformations and reproject
413
        // their long-lat bounding box into the source CRS.
414
1.00k
        const auto op_count = proj_list_get_count(op_list);
415
20.2k
        for (int i = 0; i < op_count; i++) {
416
19.2k
            auto op = proj_list_get(ctx, op_list, i);
417
19.2k
            assert(op);
418
19.2k
            double west_lon = 0.0;
419
19.2k
            double south_lat = 0.0;
420
19.2k
            double east_lon = 0.0;
421
19.2k
            double north_lat = 0.0;
422
423
19.2k
            const char *areaName = nullptr;
424
19.2k
            if (!proj_get_area_of_use(ctx, op, &west_lon, &south_lat, &east_lon,
425
19.2k
                                      &north_lat, &areaName)) {
426
0
                west_lon = -180;
427
0
                south_lat = -90;
428
0
                east_lon = 180;
429
0
                north_lat = 90;
430
0
            }
431
432
19.2k
            if (west_lon <= east_lon) {
433
18.9k
                op = add_coord_op_to_list(
434
18.9k
                    i, op, west_lon, south_lat, east_lon, north_lat,
435
18.9k
                    pjGeogToSrc, pjGeogToDst, pjSrcGeocentricToLonLat,
436
18.9k
                    pjDstGeocentricToLonLat, areaName, preparedOpList);
437
18.9k
            } else {
438
301
                auto op_clone = proj_clone(ctx, op);
439
440
301
                op = add_coord_op_to_list(
441
301
                    i, op, west_lon, south_lat, 180, north_lat, pjGeogToSrc,
442
301
                    pjGeogToDst, pjSrcGeocentricToLonLat,
443
301
                    pjDstGeocentricToLonLat, areaName, preparedOpList);
444
301
                op_clone = add_coord_op_to_list(
445
301
                    i, op_clone, -180, south_lat, east_lon, north_lat,
446
301
                    pjGeogToSrc, pjGeogToDst, pjSrcGeocentricToLonLat,
447
301
                    pjDstGeocentricToLonLat, areaName, preparedOpList);
448
301
                proj_destroy(op_clone);
449
301
            }
450
451
19.2k
            proj_destroy(op);
452
19.2k
        }
453
454
1.00k
        proj_destroy(pjGeogToSrc);
455
1.00k
        proj_destroy(pjGeogToDst);
456
1.00k
        proj_destroy(pjSrcGeocentricToLonLat);
457
1.00k
        proj_destroy(pjDstGeocentricToLonLat);
458
1.00k
        return preparedOpList;
459
1.00k
    } catch (const std::exception &) {
460
0
        proj_destroy(pjGeogToSrc);
461
0
        proj_destroy(pjGeogToDst);
462
0
        proj_destroy(pjSrcGeocentricToLonLat);
463
0
        proj_destroy(pjDstGeocentricToLonLat);
464
0
        return {};
465
0
    }
466
1.00k
}
467
468
// ---------------------------------------------------------------------------
469
470
//! @cond Doxygen_Suppress
471
static const char *getOptionValue(const char *option,
472
0
                                  const char *keyWithEqual) noexcept {
473
0
    if (ci_starts_with(option, keyWithEqual)) {
474
0
        return option + strlen(keyWithEqual);
475
0
    }
476
0
    return nullptr;
477
0
}
478
//! @endcond
479
480
/*****************************************************************************/
481
PJ *proj_create_crs_to_crs_from_pj(PJ_CONTEXT *ctx, const PJ *source_crs,
482
                                   const PJ *target_crs, PJ_AREA *area,
483
5.12k
                                   const char *const *options) {
484
    /******************************************************************************
485
        Create a transformation pipeline between two known coordinate reference
486
        systems.
487
488
        See docs/source/development/reference/functions.rst
489
490
    ******************************************************************************/
491
5.12k
    if (!ctx) {
492
0
        ctx = pj_get_default_ctx();
493
0
    }
494
5.12k
    pj_load_ini(
495
5.12k
        ctx); // to set ctx->errorIfBestTransformationNotAvailableDefault
496
497
5.12k
    const char *authority = nullptr;
498
5.12k
    double accuracy = -1;
499
5.12k
    bool allowBallparkTransformations = true;
500
5.12k
    bool forceOver = false;
501
5.12k
    bool warnIfBestTransformationNotAvailable =
502
5.12k
        ctx->warnIfBestTransformationNotAvailableDefault;
503
5.12k
    bool errorIfBestTransformationNotAvailable =
504
5.12k
        ctx->errorIfBestTransformationNotAvailableDefault;
505
5.12k
    for (auto iter = options; iter && iter[0]; ++iter) {
506
0
        const char *value;
507
0
        if ((value = getOptionValue(*iter, "AUTHORITY="))) {
508
0
            authority = value;
509
0
        } else if ((value = getOptionValue(*iter, "ACCURACY="))) {
510
0
            accuracy = pj_atof(value);
511
0
        } else if ((value = getOptionValue(*iter, "ALLOW_BALLPARK="))) {
512
0
            if (ci_equal(value, "yes"))
513
0
                allowBallparkTransformations = true;
514
0
            else if (ci_equal(value, "no"))
515
0
                allowBallparkTransformations = false;
516
0
            else {
517
0
                ctx->logger(ctx->logger_app_data, PJ_LOG_ERROR,
518
0
                            "Invalid value for ALLOW_BALLPARK option.");
519
0
                return nullptr;
520
0
            }
521
0
        } else if ((value = getOptionValue(*iter, "ONLY_BEST="))) {
522
0
            warnIfBestTransformationNotAvailable = false;
523
0
            if (ci_equal(value, "yes"))
524
0
                errorIfBestTransformationNotAvailable = true;
525
0
            else if (ci_equal(value, "no"))
526
0
                errorIfBestTransformationNotAvailable = false;
527
0
            else {
528
0
                ctx->logger(ctx->logger_app_data, PJ_LOG_ERROR,
529
0
                            "Invalid value for ONLY_BEST option.");
530
0
                return nullptr;
531
0
            }
532
0
        } else if ((value = getOptionValue(*iter, "FORCE_OVER="))) {
533
0
            if (ci_equal(value, "yes")) {
534
0
                forceOver = true;
535
0
            }
536
0
        } else {
537
0
            std::string msg("Unknown option :");
538
0
            msg += *iter;
539
0
            ctx->logger(ctx->logger_app_data, PJ_LOG_ERROR, msg.c_str());
540
0
            return nullptr;
541
0
        }
542
0
    }
543
544
5.12k
    auto operation_ctx = proj_create_operation_factory_context(ctx, authority);
545
5.12k
    if (!operation_ctx) {
546
0
        return nullptr;
547
0
    }
548
549
5.12k
    proj_operation_factory_context_set_allow_ballpark_transformations(
550
5.12k
        ctx, operation_ctx, allowBallparkTransformations);
551
552
5.12k
    if (accuracy >= 0) {
553
0
        proj_operation_factory_context_set_desired_accuracy(ctx, operation_ctx,
554
0
                                                            accuracy);
555
0
    }
556
557
5.12k
    if (area && area->bbox_set) {
558
0
        proj_operation_factory_context_set_area_of_interest(
559
0
            ctx, operation_ctx, area->west_lon_degree, area->south_lat_degree,
560
0
            area->east_lon_degree, area->north_lat_degree);
561
562
0
        if (!area->name.empty()) {
563
0
            proj_operation_factory_context_set_area_of_interest_name(
564
0
                ctx, operation_ctx, area->name.c_str());
565
0
        }
566
0
    }
567
568
5.12k
    proj_operation_factory_context_set_spatial_criterion(
569
5.12k
        ctx, operation_ctx, PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION);
570
5.12k
    proj_operation_factory_context_set_grid_availability_use(
571
5.12k
        ctx, operation_ctx,
572
5.12k
        (errorIfBestTransformationNotAvailable ||
573
5.12k
         warnIfBestTransformationNotAvailable ||
574
5.12k
         proj_context_is_network_enabled(ctx))
575
5.12k
            ? PROJ_GRID_AVAILABILITY_KNOWN_AVAILABLE
576
5.12k
            : PROJ_GRID_AVAILABILITY_DISCARD_OPERATION_IF_MISSING_GRID);
577
578
5.12k
    auto op_list =
579
5.12k
        proj_create_operations(ctx, source_crs, target_crs, operation_ctx);
580
5.12k
    proj_operation_factory_context_destroy(operation_ctx);
581
582
5.12k
    if (!op_list) {
583
732
        return nullptr;
584
732
    }
585
586
4.39k
    auto op_count = proj_list_get_count(op_list);
587
4.39k
    if (op_count == 0) {
588
50
        proj_list_destroy(op_list);
589
590
50
        proj_context_log_debug(ctx, "No operation found matching criteria");
591
50
        return nullptr;
592
50
    }
593
594
4.34k
    ctx->forceOver = forceOver;
595
596
4.34k
    const int old_debug_level = ctx->debug_level;
597
4.34k
    if (errorIfBestTransformationNotAvailable ||
598
4.34k
        warnIfBestTransformationNotAvailable)
599
0
        ctx->debug_level = PJ_LOG_NONE;
600
4.34k
    PJ *P = proj_list_get(ctx, op_list, 0);
601
4.34k
    ctx->debug_level = old_debug_level;
602
4.34k
    assert(P);
603
604
4.34k
    if (P != nullptr) {
605
4.34k
        P->errorIfBestTransformationNotAvailable =
606
4.34k
            errorIfBestTransformationNotAvailable;
607
4.34k
        P->warnIfBestTransformationNotAvailable =
608
4.34k
            warnIfBestTransformationNotAvailable;
609
4.34k
        P->skipNonInstantiable = warnIfBestTransformationNotAvailable;
610
4.34k
    }
611
612
4.34k
    const bool mayNeedToReRunWithDiscardMissing =
613
4.34k
        (errorIfBestTransformationNotAvailable ||
614
4.34k
         warnIfBestTransformationNotAvailable) &&
615
4.34k
        !proj_context_is_network_enabled(ctx);
616
4.34k
    int singleOpIsInstanciable = -1;
617
4.34k
    if (P != nullptr && op_count == 1 && mayNeedToReRunWithDiscardMissing) {
618
0
        singleOpIsInstanciable = proj_coordoperation_is_instantiable(ctx, P);
619
0
    }
620
621
4.34k
    const auto backup_errno = proj_context_errno(ctx);
622
4.34k
    if (P == nullptr ||
623
4.34k
        (op_count == 1 && (!mayNeedToReRunWithDiscardMissing ||
624
3.30k
                           errorIfBestTransformationNotAvailable ||
625
3.30k
                           singleOpIsInstanciable == static_cast<int>(true)))) {
626
3.30k
        proj_list_destroy(op_list);
627
3.30k
        ctx->forceOver = false;
628
629
3.30k
        if (P != nullptr && (errorIfBestTransformationNotAvailable ||
630
3.30k
                             warnIfBestTransformationNotAvailable)) {
631
0
            if (singleOpIsInstanciable < 0) {
632
0
                singleOpIsInstanciable =
633
0
                    proj_coordoperation_is_instantiable(ctx, P);
634
0
            }
635
0
            if (!singleOpIsInstanciable) {
636
0
                pj_warn_about_missing_grid(P);
637
0
                if (errorIfBestTransformationNotAvailable) {
638
0
                    proj_destroy(P);
639
0
                    return nullptr;
640
0
                }
641
0
            }
642
0
        }
643
644
3.30k
        if (P != nullptr) {
645
3.30k
            P->over = forceOver;
646
3.30k
        }
647
3.30k
        return P;
648
3.30k
    } else if (op_count == 1 && mayNeedToReRunWithDiscardMissing &&
649
1.03k
               !singleOpIsInstanciable) {
650
0
        pj_warn_about_missing_grid(P);
651
0
    }
652
653
1.03k
    if (errorIfBestTransformationNotAvailable ||
654
1.03k
        warnIfBestTransformationNotAvailable)
655
0
        ctx->debug_level = PJ_LOG_NONE;
656
1.03k
    auto preparedOpList =
657
1.03k
        pj_create_prepared_operations(ctx, source_crs, target_crs, op_list);
658
1.03k
    ctx->debug_level = old_debug_level;
659
660
1.03k
    ctx->forceOver = false;
661
1.03k
    proj_list_destroy(op_list);
662
663
1.03k
    if (preparedOpList.empty()) {
664
67
        proj_destroy(P);
665
67
        return nullptr;
666
67
    }
667
668
970
    bool foundInstanciableAndNonBallpark = false;
669
670
10.6k
    for (auto &op : preparedOpList) {
671
10.6k
        op.pj->over = forceOver;
672
10.6k
        op.pj->errorIfBestTransformationNotAvailable =
673
10.6k
            errorIfBestTransformationNotAvailable;
674
10.6k
        op.pj->warnIfBestTransformationNotAvailable =
675
10.6k
            warnIfBestTransformationNotAvailable;
676
10.6k
        if (mayNeedToReRunWithDiscardMissing &&
677
10.6k
            !foundInstanciableAndNonBallpark) {
678
0
            if (!proj_coordoperation_has_ballpark_transformation(op.pj->ctx,
679
0
                                                                 op.pj) &&
680
0
                op.isInstantiable()) {
681
0
                foundInstanciableAndNonBallpark = true;
682
0
            }
683
0
        }
684
10.6k
    }
685
970
    if (mayNeedToReRunWithDiscardMissing && !foundInstanciableAndNonBallpark) {
686
        // Re-run proj_create_operations with
687
        // PROJ_GRID_AVAILABILITY_DISCARD_OPERATION_IF_MISSING_GRID
688
        // Can happen for example for NAD27->NAD83 transformation when we
689
        // have no grid and thus have fallback to Helmert transformation and
690
        // a WGS84 intermediate.
691
0
        operation_ctx = proj_create_operation_factory_context(ctx, authority);
692
0
        if (operation_ctx) {
693
0
            proj_operation_factory_context_set_allow_ballpark_transformations(
694
0
                ctx, operation_ctx, allowBallparkTransformations);
695
696
0
            if (accuracy >= 0) {
697
0
                proj_operation_factory_context_set_desired_accuracy(
698
0
                    ctx, operation_ctx, accuracy);
699
0
            }
700
701
0
            if (area && area->bbox_set) {
702
0
                proj_operation_factory_context_set_area_of_interest(
703
0
                    ctx, operation_ctx, area->west_lon_degree,
704
0
                    area->south_lat_degree, area->east_lon_degree,
705
0
                    area->north_lat_degree);
706
707
0
                if (!area->name.empty()) {
708
0
                    proj_operation_factory_context_set_area_of_interest_name(
709
0
                        ctx, operation_ctx, area->name.c_str());
710
0
                }
711
0
            }
712
713
0
            proj_operation_factory_context_set_spatial_criterion(
714
0
                ctx, operation_ctx,
715
0
                PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION);
716
0
            proj_operation_factory_context_set_grid_availability_use(
717
0
                ctx, operation_ctx,
718
0
                PROJ_GRID_AVAILABILITY_DISCARD_OPERATION_IF_MISSING_GRID);
719
720
0
            op_list = proj_create_operations(ctx, source_crs, target_crs,
721
0
                                             operation_ctx);
722
0
            proj_operation_factory_context_destroy(operation_ctx);
723
724
0
            if (op_list) {
725
0
                ctx->forceOver = forceOver;
726
0
                ctx->debug_level = PJ_LOG_NONE;
727
0
                auto preparedOpList2 = pj_create_prepared_operations(
728
0
                    ctx, source_crs, target_crs, op_list);
729
0
                ctx->debug_level = old_debug_level;
730
0
                ctx->forceOver = false;
731
0
                proj_list_destroy(op_list);
732
733
0
                if (!preparedOpList2.empty()) {
734
                    // Append new lists of operations to previous one
735
0
                    std::vector<PJCoordOperation> newOpList;
736
0
                    for (auto &&op : preparedOpList) {
737
0
                        if (!proj_coordoperation_has_ballpark_transformation(
738
0
                                op.pj->ctx, op.pj)) {
739
0
                            newOpList.emplace_back(std::move(op));
740
0
                        }
741
0
                    }
742
0
                    for (auto &&op : preparedOpList2) {
743
0
                        op.pj->over = forceOver;
744
0
                        op.pj->errorIfBestTransformationNotAvailable =
745
0
                            errorIfBestTransformationNotAvailable;
746
0
                        op.pj->warnIfBestTransformationNotAvailable =
747
0
                            warnIfBestTransformationNotAvailable;
748
0
                        newOpList.emplace_back(std::move(op));
749
0
                    }
750
0
                    preparedOpList = std::move(newOpList);
751
0
                } else {
752
                    // We get there in "cs2cs --only-best --no-ballpark
753
                    // EPSG:4326+3855 EPSG:4979" use case, where the initial
754
                    // create_operations returned 1 operation, and the retry
755
                    // with
756
                    // PROJ_GRID_AVAILABILITY_DISCARD_OPERATION_IF_MISSING_GRID
757
                    // returned 0.
758
0
                    if (op_count == 1 &&
759
0
                        errorIfBestTransformationNotAvailable) {
760
0
                        if (singleOpIsInstanciable < 0) {
761
0
                            singleOpIsInstanciable =
762
0
                                proj_coordoperation_is_instantiable(ctx, P);
763
0
                        }
764
0
                        if (!singleOpIsInstanciable) {
765
0
                            proj_destroy(P);
766
0
                            proj_context_errno_set(ctx, backup_errno);
767
0
                            return nullptr;
768
0
                        }
769
0
                    }
770
0
                }
771
0
            }
772
0
        }
773
0
    }
774
775
    // If there's finally juste a single result, return it directly
776
970
    if (preparedOpList.size() == 1) {
777
304
        auto retP = preparedOpList[0].pj;
778
304
        preparedOpList[0].pj = nullptr;
779
304
        proj_destroy(P);
780
304
        return retP;
781
304
    }
782
783
666
    P->alternativeCoordinateOperations = std::move(preparedOpList);
784
    // The returned P is rather dummy
785
666
    P->descr = "Set of coordinate operations";
786
666
    P->over = forceOver;
787
666
    P->iso_obj = nullptr;
788
666
    P->fwd = nullptr;
789
666
    P->inv = nullptr;
790
666
    P->fwd3d = nullptr;
791
666
    P->inv3d = nullptr;
792
666
    P->fwd4d = nullptr;
793
666
    P->inv4d = nullptr;
794
795
666
    return P;
796
970
}