Coverage Report

Created: 2026-05-16 07:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/PROJ/src/iso19111/operation/concatenatedoperation.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  PROJ
4
 * Purpose:  ISO19111:2019 implementation
5
 * Author:   Even Rouault <even dot rouault at spatialys dot com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2018, Even Rouault <even dot rouault at spatialys dot 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
#ifndef FROM_PROJ_CPP
30
#define FROM_PROJ_CPP
31
#endif
32
33
#include "proj/common.hpp"
34
#include "proj/coordinateoperation.hpp"
35
#include "proj/crs.hpp"
36
#include "proj/io.hpp"
37
#include "proj/metadata.hpp"
38
#include "proj/util.hpp"
39
40
#include "proj/internal/crs_internal.hpp"
41
#include "proj/internal/internal.hpp"
42
#include "proj/internal/io_internal.hpp"
43
44
#include "coordinateoperation_internal.hpp"
45
#include "oputils.hpp"
46
47
// PROJ include order is sensitive
48
// clang-format off
49
#include "proj.h"
50
#include "proj_internal.h" // M_PI
51
// clang-format on
52
#include "proj_constants.h"
53
54
#include "proj_json_streaming_writer.hpp"
55
56
#include <algorithm>
57
#include <cassert>
58
#include <cmath>
59
#include <cstring>
60
#include <memory>
61
#include <set>
62
#include <string>
63
#include <vector>
64
65
using namespace NS_PROJ::internal;
66
67
// ---------------------------------------------------------------------------
68
69
NS_PROJ_START
70
namespace operation {
71
72
// ---------------------------------------------------------------------------
73
74
//! @cond Doxygen_Suppress
75
struct ConcatenatedOperation::Private {
76
    std::vector<CoordinateOperationNNPtr> operations_{};
77
    bool computedName_ = false;
78
79
    explicit Private(const std::vector<CoordinateOperationNNPtr> &operationsIn)
80
209k
        : operations_(operationsIn) {}
81
13.2k
    Private(const Private &) = default;
82
};
83
//! @endcond
84
85
// ---------------------------------------------------------------------------
86
87
//! @cond Doxygen_Suppress
88
223k
ConcatenatedOperation::~ConcatenatedOperation() = default;
89
//! @endcond
90
91
// ---------------------------------------------------------------------------
92
93
//! @cond Doxygen_Suppress
94
ConcatenatedOperation::ConcatenatedOperation(const ConcatenatedOperation &other)
95
13.2k
    : CoordinateOperation(other), d(std::make_unique<Private>(*(other.d))) {}
96
//! @endcond
97
98
// ---------------------------------------------------------------------------
99
100
ConcatenatedOperation::ConcatenatedOperation(
101
    const std::vector<CoordinateOperationNNPtr> &operationsIn)
102
209k
    : CoordinateOperation(), d(std::make_unique<Private>(operationsIn)) {
103
594k
    for (const auto &op : operationsIn) {
104
594k
        if (op->requiresPerCoordinateInputTime()) {
105
2.36k
            setRequiresPerCoordinateInputTime(true);
106
2.36k
            break;
107
2.36k
        }
108
594k
    }
109
209k
}
110
111
// ---------------------------------------------------------------------------
112
113
/** \brief Return the operation steps of the concatenated operation.
114
 *
115
 * @return the operation steps.
116
 */
117
const std::vector<CoordinateOperationNNPtr> &
118
734k
ConcatenatedOperation::operations() const {
119
734k
    return d->operations_;
120
734k
}
121
122
// ---------------------------------------------------------------------------
123
124
//! @cond Doxygen_Suppress
125
634k
static bool areCRSMoreOrLessEquivalent(const crs::CRS *a, const crs::CRS *b) {
126
634k
    const auto &aIds = a->identifiers();
127
634k
    const auto &bIds = b->identifiers();
128
634k
    if (aIds.size() == 1 && bIds.size() == 1 &&
129
461k
        aIds[0]->code() == bIds[0]->code() &&
130
460k
        *aIds[0]->codeSpace() == *bIds[0]->codeSpace()) {
131
460k
        return true;
132
460k
    }
133
174k
    if (a->_isEquivalentTo(b, util::IComparable::Criterion::EQUIVALENT)) {
134
173k
        return true;
135
173k
    }
136
    // This is for example for EPSG:10146 which is EPSG:9471
137
    // (INAGeoid2020 v1 height)
138
    // to EPSG:20036 (INAGeoid2020 v2 height), but chains
139
    // EPSG:9629 (SRGI2013 to SRGI2013 + INAGeoid2020 v1 height (1))
140
    // with EPSG:10145 (SRGI2013 to SRGI2013 + INAGeoid2020 v2 height (1))
141
174
    const auto compoundA = dynamic_cast<const crs::CompoundCRS *>(a);
142
174
    const auto compoundB = dynamic_cast<const crs::CompoundCRS *>(b);
143
174
    if (compoundA && !compoundB)
144
10
        return areCRSMoreOrLessEquivalent(
145
10
            compoundA->componentReferenceSystems()[1].get(), b);
146
164
    else if (!compoundA && compoundB)
147
2
        return areCRSMoreOrLessEquivalent(
148
2
            a, compoundB->componentReferenceSystems()[1].get());
149
162
    return false;
150
174
}
151
//! @endcond
152
153
// ---------------------------------------------------------------------------
154
155
/** \brief Instantiate a ConcatenatedOperation
156
 *
157
 * @param properties See \ref general_properties. At minimum the name should
158
 * be
159
 * defined.
160
 * @param operationsIn Vector of the CoordinateOperation steps.
161
 * @param accuracies Vector of positional accuracy (might be empty).
162
 * @return new Transformation.
163
 * @throws InvalidOperation if the object cannot be constructed.
164
 */
165
ConcatenatedOperationNNPtr ConcatenatedOperation::create(
166
    const util::PropertyMap &properties,
167
    const std::vector<CoordinateOperationNNPtr> &operationsIn,
168
    const std::vector<metadata::PositionalAccuracyNNPtr>
169
        &accuracies) // throw InvalidOperation
170
209k
{
171
209k
    if (operationsIn.size() < 2) {
172
0
        throw InvalidOperation(
173
0
            "ConcatenatedOperation must have at least 2 operations");
174
0
    }
175
209k
    crs::CRSPtr lastTargetCRS;
176
177
209k
    crs::CRSPtr interpolationCRS;
178
209k
    bool interpolationCRSValid = true;
179
209k
    bool hasBallparkTransformation = false;
180
808k
    for (size_t i = 0; i < operationsIn.size(); i++) {
181
598k
        auto l_sourceCRS = operationsIn[i]->sourceCRS();
182
598k
        auto l_targetCRS = operationsIn[i]->targetCRS();
183
184
598k
        hasBallparkTransformation |=
185
598k
            operationsIn[i]->hasBallparkTransformation();
186
598k
        if (interpolationCRSValid) {
187
598k
            auto subOpInterpCRS = operationsIn[i]->interpolationCRS();
188
598k
            if (interpolationCRS == nullptr)
189
598k
                interpolationCRS = std::move(subOpInterpCRS);
190
158
            else if (subOpInterpCRS == nullptr ||
191
158
                     !(subOpInterpCRS->isEquivalentTo(
192
158
                         interpolationCRS.get(),
193
158
                         util::IComparable::Criterion::EQUIVALENT))) {
194
154
                interpolationCRS = nullptr;
195
154
                interpolationCRSValid = false;
196
154
            }
197
598k
        }
198
199
598k
        if (l_sourceCRS == nullptr || l_targetCRS == nullptr) {
200
0
            throw InvalidOperation("At least one of the operation lacks a "
201
0
                                   "source and/or target CRS");
202
0
        }
203
598k
        if (i >= 1) {
204
388k
            if (!areCRSMoreOrLessEquivalent(l_sourceCRS.get(),
205
388k
                                            lastTargetCRS.get())) {
206
#ifdef DEBUG_CONCATENATED_OPERATION
207
                std::cerr << "Step " << i - 1 << ": "
208
                          << operationsIn[i - 1]->nameStr() << std::endl;
209
                std::cerr << "Step " << i << ": " << operationsIn[i]->nameStr()
210
                          << std::endl;
211
                {
212
                    auto f(io::WKTFormatter::create(
213
                        io::WKTFormatter::Convention::WKT2_2019));
214
                    std::cerr << "Source CRS of step " << i << ":" << std::endl;
215
                    std::cerr << l_sourceCRS->exportToWKT(f.get()) << std::endl;
216
                }
217
                {
218
                    auto f(io::WKTFormatter::create(
219
                        io::WKTFormatter::Convention::WKT2_2019));
220
                    std::cerr << "Target CRS of step " << i - 1 << ":"
221
                              << std::endl;
222
                    std::cerr << lastTargetCRS->exportToWKT(f.get())
223
                              << std::endl;
224
                }
225
#endif
226
0
                throw InvalidOperation(
227
0
                    "Inconsistent chaining of CRS in operations");
228
0
            }
229
388k
        }
230
598k
        lastTargetCRS = std::move(l_targetCRS);
231
598k
    }
232
233
    // When chaining VerticalCRS -> GeographicCRS -> VerticalCRS, use
234
    // GeographicCRS as the interpolationCRS
235
209k
    const auto l_sourceCRS = NN_NO_CHECK(operationsIn[0]->sourceCRS());
236
209k
    const auto l_targetCRS = NN_NO_CHECK(operationsIn.back()->targetCRS());
237
209k
    if (operationsIn.size() == 2 && interpolationCRS == nullptr &&
238
120k
        dynamic_cast<const crs::VerticalCRS *>(l_sourceCRS.get()) != nullptr &&
239
651
        dynamic_cast<const crs::VerticalCRS *>(l_targetCRS.get()) != nullptr) {
240
258
        const auto geog1 = dynamic_cast<crs::GeographicCRS *>(
241
258
            operationsIn[0]->targetCRS().get());
242
258
        const auto geog2 = dynamic_cast<crs::GeographicCRS *>(
243
258
            operationsIn[1]->sourceCRS().get());
244
258
        if (geog1 != nullptr && geog2 != nullptr &&
245
252
            geog1->_isEquivalentTo(geog2,
246
252
                                   util::IComparable::Criterion::EQUIVALENT)) {
247
252
            interpolationCRS = operationsIn[0]->targetCRS();
248
252
        }
249
258
    }
250
251
209k
    auto op = ConcatenatedOperation::nn_make_shared<ConcatenatedOperation>(
252
209k
        operationsIn);
253
209k
    op->assignSelf(op);
254
209k
    op->setProperties(properties);
255
209k
    op->setHasBallparkTransformation(hasBallparkTransformation);
256
209k
    op->setCRSs(l_sourceCRS, l_targetCRS, interpolationCRS);
257
209k
    op->setAccuracies(accuracies);
258
#ifdef DEBUG_CONCATENATED_OPERATION
259
    {
260
        auto f(
261
            io::WKTFormatter::create(io::WKTFormatter::Convention::WKT2_2019));
262
        std::cerr << "ConcatenatedOperation::create()" << std::endl;
263
        std::cerr << op->exportToWKT(f.get()) << std::endl;
264
    }
265
#endif
266
209k
    return op;
267
209k
}
268
269
// ---------------------------------------------------------------------------
270
271
//! @cond Doxygen_Suppress
272
273
// ---------------------------------------------------------------------------
274
275
/* static */ void
276
ConcatenatedOperation::setCRSsUpdateInverse(CoordinateOperation *co,
277
                                            const crs::CRSNNPtr &sourceCRS,
278
32
                                            const crs::CRSNNPtr &targetCRS) {
279
280
32
    co->setCRSsUpdateInverse(sourceCRS, targetCRS, co->interpolationCRS());
281
32
}
282
283
// ---------------------------------------------------------------------------
284
285
void ConcatenatedOperation::fixSteps(
286
    const crs::CRSNNPtr &concatOpSourceCRS,
287
    const crs::CRSNNPtr &concatOpTargetCRS,
288
    std::vector<CoordinateOperationNNPtr> &operationsInOut,
289
41.4k
    const io::DatabaseContextPtr & /*dbContext*/, bool fixDirectionAllowed) {
290
291
    // Set of heuristics to assign CRS to steps, and possibly reverse them.
292
293
41.4k
    const auto isGeographic = [](const crs::CRS *crs) -> bool {
294
31
        return dynamic_cast<const crs::GeographicCRS *>(crs) != nullptr;
295
31
    };
296
297
41.4k
    const auto isGeocentric = [](const crs::CRS *crs) -> bool {
298
19
        const auto geodCRS = dynamic_cast<const crs::GeodeticCRS *>(crs);
299
19
        return (geodCRS && geodCRS->isGeocentric());
300
19
    };
301
302
    // Apply axis order reversal operation on first operation if needed
303
    // to set CRSs on it
304
41.4k
    if (operationsInOut.size() >= 1) {
305
41.4k
        auto &op = operationsInOut.front();
306
41.4k
        auto l_sourceCRS = op->sourceCRS();
307
41.4k
        auto l_targetCRS = op->targetCRS();
308
41.4k
        auto conv = dynamic_cast<const Conversion *>(op.get());
309
41.4k
        if (conv && !l_sourceCRS && !l_targetCRS &&
310
15
            isAxisOrderReversal(conv->method()->getEPSGCode())) {
311
0
            auto reversedCRS = concatOpSourceCRS->applyAxisOrderReversal(
312
0
                NORMALIZED_AXIS_ORDER_SUFFIX_STR);
313
0
            setCRSsUpdateInverse(op.get(), concatOpSourceCRS, reversedCRS);
314
0
        }
315
41.4k
    }
316
317
    // Apply axis order reversal operation on last operation if needed
318
    // to set CRSs on it
319
41.4k
    if (operationsInOut.size() >= 2) {
320
41.4k
        auto &op = operationsInOut.back();
321
41.4k
        auto l_sourceCRS = op->sourceCRS();
322
41.4k
        auto l_targetCRS = op->targetCRS();
323
41.4k
        auto conv = dynamic_cast<const Conversion *>(op.get());
324
41.4k
        if (conv && !l_sourceCRS && !l_targetCRS &&
325
13
            isAxisOrderReversal(conv->method()->getEPSGCode())) {
326
0
            auto reversedCRS = concatOpTargetCRS->applyAxisOrderReversal(
327
0
                NORMALIZED_AXIS_ORDER_SUFFIX_STR);
328
0
            setCRSsUpdateInverse(op.get(), reversedCRS, concatOpTargetCRS);
329
0
        }
330
41.4k
    }
331
332
    // If the first operation is a transformation whose target CRS matches the
333
    // source CRS of the concatenated operation, then reverse it.
334
41.4k
    if (fixDirectionAllowed && operationsInOut.size() >= 2) {
335
39.6k
        auto &op = operationsInOut.front();
336
39.6k
        auto l_sourceCRS = op->sourceCRS();
337
39.6k
        auto l_targetCRS = op->targetCRS();
338
39.6k
        if (l_sourceCRS && l_targetCRS &&
339
39.6k
            !areCRSMoreOrLessEquivalent(l_sourceCRS.get(),
340
39.6k
                                        concatOpSourceCRS.get()) &&
341
32
            areCRSMoreOrLessEquivalent(l_targetCRS.get(),
342
32
                                       concatOpSourceCRS.get())) {
343
32
            op = op->inverse();
344
32
        }
345
39.6k
    }
346
347
    // If the last operation is a transformation whose source CRS matches the
348
    // target CRS of the concatenated operation, then reverse it.
349
41.4k
    if (fixDirectionAllowed && operationsInOut.size() >= 2) {
350
39.6k
        auto &op = operationsInOut.back();
351
39.6k
        auto l_sourceCRS = op->sourceCRS();
352
39.6k
        auto l_targetCRS = op->targetCRS();
353
39.6k
        if (l_sourceCRS && l_targetCRS &&
354
39.6k
            !areCRSMoreOrLessEquivalent(l_targetCRS.get(),
355
39.6k
                                        concatOpTargetCRS.get()) &&
356
54
            areCRSMoreOrLessEquivalent(l_sourceCRS.get(),
357
54
                                       concatOpTargetCRS.get())) {
358
54
            op = op->inverse();
359
54
        }
360
39.6k
    }
361
362
41.4k
    const auto extractDerivedCRS =
363
41.4k
        [](const crs::CRS *crs) -> const crs::DerivedCRS * {
364
41
        auto derivedCRS = dynamic_cast<const crs::DerivedCRS *>(crs);
365
41
        if (derivedCRS)
366
10
            return derivedCRS;
367
31
        auto compoundCRS = dynamic_cast<const crs::CompoundCRS *>(crs);
368
31
        if (compoundCRS) {
369
0
            derivedCRS = dynamic_cast<const crs::DerivedCRS *>(
370
0
                compoundCRS->componentReferenceSystems().front().get());
371
0
            if (derivedCRS)
372
0
                return derivedCRS;
373
0
        }
374
31
        return nullptr;
375
31
    };
376
377
125k
    for (size_t i = 0; i < operationsInOut.size(); ++i) {
378
84.2k
        auto &op = operationsInOut[i];
379
84.2k
        auto l_sourceCRS = op->sourceCRS();
380
84.2k
        auto l_targetCRS = op->targetCRS();
381
84.2k
        auto conv = dynamic_cast<const Conversion *>(op.get());
382
84.2k
        if (conv && i == 0 && !l_sourceCRS && !l_targetCRS) {
383
15
            if (auto derivedCRS = extractDerivedCRS(concatOpSourceCRS.get())) {
384
0
                if (i + 1 < operationsInOut.size()) {
385
                    // use the sourceCRS of the next operation as our target CRS
386
0
                    l_targetCRS = operationsInOut[i + 1]->sourceCRS();
387
                    // except if it looks like the next operation should
388
                    // actually be reversed !!!
389
0
                    if (l_targetCRS &&
390
0
                        !areCRSMoreOrLessEquivalent(
391
0
                            l_targetCRS.get(), derivedCRS->baseCRS().get()) &&
392
0
                        operationsInOut[i + 1]->targetCRS() &&
393
0
                        areCRSMoreOrLessEquivalent(
394
0
                            operationsInOut[i + 1]->targetCRS().get(),
395
0
                            derivedCRS->baseCRS().get())) {
396
0
                        l_targetCRS = operationsInOut[i + 1]->targetCRS();
397
0
                    }
398
0
                }
399
0
                if (!l_targetCRS) {
400
0
                    l_targetCRS = derivedCRS->baseCRS().as_nullable();
401
0
                }
402
0
                auto invConv =
403
0
                    util::nn_dynamic_pointer_cast<InverseConversion>(op);
404
0
                auto nn_targetCRS = NN_NO_CHECK(l_targetCRS);
405
0
                if (invConv) {
406
0
                    setCRSsUpdateInverse(op.get(), concatOpSourceCRS,
407
0
                                         nn_targetCRS);
408
0
                } else if (fixDirectionAllowed) {
409
0
                    op->setCRSs(nn_targetCRS, concatOpSourceCRS, nullptr);
410
0
                    op = op->inverse();
411
0
                }
412
15
            } else if (i + 1 < operationsInOut.size()) {
413
                /* coverity[copy_paste_error] */
414
15
                l_targetCRS = operationsInOut[i + 1]->sourceCRS();
415
15
                if (l_targetCRS) {
416
15
                    setCRSsUpdateInverse(op.get(), concatOpSourceCRS,
417
15
                                         NN_NO_CHECK(l_targetCRS));
418
15
                }
419
15
            }
420
84.2k
        } else if (conv && i + 1 == operationsInOut.size() && !l_sourceCRS &&
421
13
                   !l_targetCRS) {
422
13
            auto derivedCRS = extractDerivedCRS(concatOpTargetCRS.get());
423
13
            if (derivedCRS) {
424
0
                if (i >= 1) {
425
                    // use the targetCRS of the previous operation as our source
426
                    // CRS
427
0
                    l_sourceCRS = operationsInOut[i - 1]->targetCRS();
428
                    // except if it looks like the previous operation should
429
                    // actually be reversed !!!
430
0
                    if (l_sourceCRS &&
431
0
                        !areCRSMoreOrLessEquivalent(
432
0
                            l_sourceCRS.get(), derivedCRS->baseCRS().get()) &&
433
0
                        operationsInOut[i - 1]->sourceCRS() &&
434
0
                        areCRSMoreOrLessEquivalent(
435
0
                            operationsInOut[i - 1]->sourceCRS().get(),
436
0
                            derivedCRS->baseCRS().get())) {
437
0
                        l_sourceCRS = operationsInOut[i - 1]->sourceCRS();
438
0
                        operationsInOut[i - 1] =
439
0
                            operationsInOut[i - 1]->inverse();
440
0
                    }
441
0
                }
442
0
                if (!l_sourceCRS) {
443
0
                    l_sourceCRS = derivedCRS->baseCRS().as_nullable();
444
0
                }
445
0
                setCRSsUpdateInverse(op.get(), NN_NO_CHECK(l_sourceCRS),
446
0
                                     concatOpTargetCRS);
447
13
            } else if (i >= 1) {
448
13
                l_sourceCRS = operationsInOut[i - 1]->targetCRS();
449
13
                if (l_sourceCRS) {
450
13
                    derivedCRS = extractDerivedCRS(l_sourceCRS.get());
451
13
                    if (fixDirectionAllowed && derivedCRS &&
452
3
                        conv->isEquivalentTo(
453
3
                            derivedCRS->derivingConversion().get(),
454
3
                            util::IComparable::Criterion::EQUIVALENT)) {
455
3
                        op = op->inverse();
456
3
                    }
457
13
                    setCRSsUpdateInverse(op.get(), NN_NO_CHECK(l_sourceCRS),
458
13
                                         concatOpTargetCRS);
459
13
                }
460
13
            }
461
84.2k
        } else if (conv && i > 0 && i < operationsInOut.size() - 1) {
462
463
4
            l_sourceCRS = operationsInOut[i - 1]->targetCRS();
464
4
            l_targetCRS = operationsInOut[i + 1]->sourceCRS();
465
            // For an intermediate conversion, use the target CRS of the
466
            // previous step and the source CRS of the next step
467
4
            if (l_sourceCRS && l_targetCRS) {
468
                // If the sourceCRS is a projectedCRS and the target a
469
                // geographic one, then we must inverse the operation. See
470
                // https://github.com/OSGeo/PROJ/issues/2817
471
2
                if (fixDirectionAllowed &&
472
0
                    dynamic_cast<const crs::ProjectedCRS *>(
473
0
                        l_sourceCRS.get()) &&
474
0
                    dynamic_cast<const crs::GeographicCRS *>(
475
0
                        l_targetCRS.get())) {
476
0
                    op = op->inverse();
477
0
                    setCRSsUpdateInverse(op.get(), NN_NO_CHECK(l_sourceCRS),
478
0
                                         NN_NO_CHECK(l_targetCRS));
479
2
                } else {
480
2
                    setCRSsUpdateInverse(op.get(), NN_NO_CHECK(l_sourceCRS),
481
2
                                         NN_NO_CHECK(l_targetCRS));
482
483
                    // Deal with special case of
484
                    // https://github.com/OSGeo/PROJ/issues/4116 where EPSG:7989
485
                    // -- NAVD88 height to NAVD88 depth conversion is chained
486
                    // with "NAD83(FBN)+LMSL to NAD83(FBN)+NAVD88 depth" The
487
                    // latter must thus be inversed
488
2
                    const auto nPosTo = conv->nameStr().find(" to ");
489
2
                    const auto nPosToNextOp =
490
2
                        operationsInOut[i + 1]->nameStr().find(" to ");
491
2
                    if (fixDirectionAllowed && nPosTo != std::string::npos &&
492
0
                        nPosToNextOp != std::string::npos) {
493
0
                        const std::string convTo =
494
0
                            conv->nameStr().substr(nPosTo + strlen(" to "));
495
0
                        const std::string nextOpFrom =
496
0
                            operationsInOut[i + 1]->nameStr().substr(
497
0
                                0, nPosToNextOp);
498
0
                        const std::string nextOpTo =
499
0
                            operationsInOut[i + 1]->nameStr().substr(
500
0
                                nPosToNextOp + strlen(" to "));
501
0
                        if (nextOpTo.find(convTo) != std::string::npos &&
502
0
                            nextOpFrom.find(convTo) == std::string::npos &&
503
0
                            operationsInOut[i + 1]->sourceCRS()) {
504
0
                            operationsInOut[i + 1] =
505
0
                                operationsInOut[i + 1]->inverse();
506
507
0
                            setCRSsUpdateInverse(
508
0
                                op.get(), NN_NO_CHECK(l_sourceCRS),
509
0
                                NN_NO_CHECK(
510
0
                                    operationsInOut[i + 1]->sourceCRS()));
511
0
                        }
512
0
                    }
513
2
                }
514
2
            } else if (l_sourceCRS && l_targetCRS == nullptr &&
515
2
                       conv->method()->getEPSGCode() ==
516
2
                           EPSG_CODE_METHOD_HEIGHT_DEPTH_REVERSAL) {
517
                // Needed for EPSG:7987 e.g.
518
2
                auto vertCRS =
519
2
                    dynamic_cast<const crs::VerticalCRS *>(l_sourceCRS.get());
520
2
                if (vertCRS && ends_with(l_sourceCRS->nameStr(), " height") &&
521
2
                    &vertCRS->coordinateSystem()->axisList()[0]->direction() ==
522
2
                        &cs::AxisDirection::UP) {
523
2
                    setCRSsUpdateInverse(
524
2
                        op.get(), NN_NO_CHECK(l_sourceCRS),
525
2
                        crs::VerticalCRS::create(
526
2
                            util::PropertyMap().set(
527
2
                                common::IdentifiedObject::NAME_KEY,
528
2
                                l_sourceCRS->nameStr().substr(
529
2
                                    0, l_sourceCRS->nameStr().size() -
530
2
                                           strlen(" height")) +
531
2
                                    " depth"),
532
2
                            vertCRS->datum(), vertCRS->datumEnsemble(),
533
2
                            cs::VerticalCS::create(
534
2
                                util::PropertyMap(),
535
2
                                cs::CoordinateSystemAxis::create(
536
2
                                    util::PropertyMap().set(
537
2
                                        common::IdentifiedObject::NAME_KEY,
538
2
                                        "Gravity-related depth"),
539
2
                                    "D", cs::AxisDirection::DOWN,
540
2
                                    vertCRS->coordinateSystem()
541
2
                                        ->axisList()[0]
542
2
                                        ->unit()))));
543
2
                }
544
2
            }
545
84.2k
        } else if (!conv && l_sourceCRS && l_targetCRS) {
546
547
            // Transformations might be mentioned in their forward directions,
548
            // whereas we should instead use the reverse path.
549
84.2k
            auto prevOpTarget = (i == 0) ? concatOpSourceCRS.as_nullable()
550
84.2k
                                         : operationsInOut[i - 1]->targetCRS();
551
84.2k
            if (prevOpTarget == nullptr) {
552
0
                throw InvalidOperation(
553
0
                    "Cannot determine targetCRS of operation at step " +
554
0
                    toString(static_cast<int>(i)));
555
0
            }
556
84.2k
            if (areCRSMoreOrLessEquivalent(l_sourceCRS.get(),
557
84.2k
                                           prevOpTarget.get())) {
558
                // do nothing
559
84.2k
            } else if (fixDirectionAllowed &&
560
57
                       areCRSMoreOrLessEquivalent(l_targetCRS.get(),
561
57
                                                  prevOpTarget.get())) {
562
38
                op = op->inverse();
563
38
            }
564
            // Below is needed for EPSG:9103 which chains NAD83(2011) geographic
565
            // 2D with NAD83(2011) geocentric
566
19
            else if (l_sourceCRS->nameStr() == prevOpTarget->nameStr() &&
567
7
                     ((isGeographic(l_sourceCRS.get()) &&
568
7
                       isGeocentric(prevOpTarget.get())) ||
569
0
                      (isGeocentric(l_sourceCRS.get()) &&
570
7
                       isGeographic(prevOpTarget.get())))) {
571
7
                auto newOp(Conversion::createGeographicGeocentric(
572
7
                    NN_NO_CHECK(prevOpTarget), NN_NO_CHECK(l_sourceCRS)));
573
7
                operationsInOut.insert(operationsInOut.begin() + i, newOp);
574
12
            } else if (l_targetCRS->nameStr() == prevOpTarget->nameStr() &&
575
12
                       ((isGeographic(l_targetCRS.get()) &&
576
0
                         isGeocentric(prevOpTarget.get())) ||
577
12
                        (isGeocentric(l_targetCRS.get()) &&
578
12
                         isGeographic(prevOpTarget.get())))) {
579
12
                auto newOp(Conversion::createGeographicGeocentric(
580
12
                    NN_NO_CHECK(prevOpTarget), NN_NO_CHECK(l_targetCRS)));
581
12
                operationsInOut.insert(operationsInOut.begin() + i, newOp);
582
                // Particular case for https://github.com/OSGeo/PROJ/issues/3819
583
                // where the antepenultimate transformation goes to A
584
                // (geographic 3D) // and the last transformation is a NADCON 3D
585
                // but between A (geographic 2D) to B (geographic 2D), and the
586
                // concatenated transformation target CRS is B (geographic 3D)
587
                // This is due to an oddity of the EPSG database that registers
588
                // the NADCON 3D transformation between the 2D geographic CRS
589
                // and not the 3D ones.
590
12
            } else if (i + 1 == operationsInOut.size() &&
591
0
                       l_sourceCRS->nameStr() == prevOpTarget->nameStr() &&
592
0
                       l_targetCRS->nameStr() == concatOpTargetCRS->nameStr() &&
593
0
                       isGeographic(l_targetCRS.get()) &&
594
0
                       isGeographic(concatOpTargetCRS.get()) &&
595
0
                       isGeographic(l_sourceCRS.get()) &&
596
0
                       isGeographic(prevOpTarget.get()) &&
597
0
                       dynamic_cast<const crs::GeographicCRS *>(
598
0
                           prevOpTarget.get())
599
0
                               ->coordinateSystem()
600
0
                               ->axisList()
601
0
                               .size() == 3 &&
602
0
                       dynamic_cast<const crs::GeographicCRS *>(
603
0
                           l_sourceCRS.get())
604
0
                               ->coordinateSystem()
605
0
                               ->axisList()
606
0
                               .size() == 2 &&
607
0
                       dynamic_cast<const crs::GeographicCRS *>(
608
0
                           l_targetCRS.get())
609
0
                               ->coordinateSystem()
610
0
                               ->axisList()
611
0
                               .size() == 2) {
612
0
                const auto transf =
613
0
                    dynamic_cast<const Transformation *>(op.get());
614
0
                if (transf &&
615
0
                    (transf->method()->getEPSGCode() ==
616
0
                         EPSG_CODE_METHOD_NADCON5_3D ||
617
0
                     transf->parameterValue(
618
0
                         PROJ_WKT2_PARAMETER_LATITUDE_LONGITUDE_ELLIPOISDAL_HEIGHT_DIFFERENCE_FILE,
619
0
                         0))) {
620
0
                    setCRSsUpdateInverse(op.get(), NN_NO_CHECK(prevOpTarget),
621
0
                                         concatOpTargetCRS);
622
0
                }
623
0
            }
624
84.2k
        }
625
84.2k
    }
626
627
41.4k
    if (!operationsInOut.empty()) {
628
41.4k
        auto l_sourceCRS = operationsInOut.front()->sourceCRS();
629
41.4k
        if (l_sourceCRS && !areCRSMoreOrLessEquivalent(
630
41.4k
                               l_sourceCRS.get(), concatOpSourceCRS.get())) {
631
0
            throw InvalidOperation("The source CRS of the first step of "
632
0
                                   "concatenated operation is not the same "
633
0
                                   "as the source CRS of the concatenated "
634
0
                                   "operation itself");
635
0
        }
636
637
41.4k
        auto l_targetCRS = operationsInOut.back()->targetCRS();
638
41.4k
        if (l_targetCRS && !areCRSMoreOrLessEquivalent(
639
41.4k
                               l_targetCRS.get(), concatOpTargetCRS.get())) {
640
0
            if (l_targetCRS->nameStr() == concatOpTargetCRS->nameStr() &&
641
0
                ((isGeographic(l_targetCRS.get()) &&
642
0
                  isGeocentric(concatOpTargetCRS.get())) ||
643
0
                 (isGeocentric(l_targetCRS.get()) &&
644
0
                  isGeographic(concatOpTargetCRS.get())))) {
645
0
                auto newOp(Conversion::createGeographicGeocentric(
646
0
                    NN_NO_CHECK(l_targetCRS), concatOpTargetCRS));
647
0
                operationsInOut.push_back(newOp);
648
0
            } else {
649
0
                throw InvalidOperation("The target CRS of the last step of "
650
0
                                       "concatenated operation is not the same "
651
0
                                       "as the target CRS of the concatenated "
652
0
                                       "operation itself");
653
0
            }
654
0
        }
655
41.4k
    }
656
41.4k
}
657
//! @endcond
658
659
// ---------------------------------------------------------------------------
660
661
/** \brief Instantiate a ConcatenatedOperation, or return a single
662
 * coordinate
663
 * operation.
664
 *
665
 * This computes its accuracy from the sum of its member operations, its
666
 * extent
667
 *
668
 * @param operationsIn Vector of the CoordinateOperation steps.
669
 * @param checkExtent Whether we should check the non-emptiness of the
670
 * intersection
671
 * of the extents of the operations
672
 * @throws InvalidOperation if the object cannot be constructed.
673
 */
674
CoordinateOperationNNPtr ConcatenatedOperation::createComputeMetadata(
675
    const std::vector<CoordinateOperationNNPtr> &operationsIn,
676
    bool checkExtent) // throw InvalidOperation
677
174k
{
678
174k
    util::PropertyMap properties;
679
680
174k
    if (operationsIn.size() == 1) {
681
54.6k
        return operationsIn[0];
682
54.6k
    }
683
684
119k
    std::vector<CoordinateOperationNNPtr> flattenOps;
685
119k
    bool hasBallparkTransformation = false;
686
274k
    for (const auto &subOp : operationsIn) {
687
274k
        hasBallparkTransformation |= subOp->hasBallparkTransformation();
688
274k
        auto subOpConcat =
689
274k
            dynamic_cast<const ConcatenatedOperation *>(subOp.get());
690
274k
        if (subOpConcat) {
691
79.7k
            auto subOps = subOpConcat->operations();
692
212k
            for (const auto &subSubOp : subOps) {
693
212k
                flattenOps.emplace_back(subSubOp);
694
212k
            }
695
195k
        } else {
696
195k
            flattenOps.emplace_back(subOp);
697
195k
        }
698
274k
    }
699
700
    // Remove consecutive inverse operations
701
119k
    if (flattenOps.size() > 2) {
702
69.7k
        std::vector<size_t> indices;
703
377k
        for (size_t i = 0; i < flattenOps.size(); ++i)
704
308k
            indices.push_back(i);
705
69.9k
        while (true) {
706
69.9k
            bool bHasChanged = false;
707
307k
            for (size_t i = 0; i + 1 < indices.size(); ++i) {
708
238k
                if (flattenOps[indices[i]]->_isEquivalentTo(
709
238k
                        flattenOps[indices[i + 1]]->inverse().get(),
710
238k
                        util::IComparable::Criterion::EQUIVALENT) &&
711
844
                    flattenOps[indices[i]]->sourceCRS()->_isEquivalentTo(
712
844
                        flattenOps[indices[i + 1]]->targetCRS().get(),
713
844
                        util::IComparable::Criterion::EQUIVALENT)) {
714
586
                    indices.erase(indices.begin() + i, indices.begin() + i + 2);
715
586
                    bHasChanged = true;
716
586
                    break;
717
586
                }
718
238k
            }
719
            // We bail out if indices.size() == 2, because potentially
720
            // the last 2 remaining ones could auto-cancel, and we would have
721
            // to have a special case for that (and this happens in practice).
722
69.9k
            if (!bHasChanged || indices.size() <= 2)
723
69.7k
                break;
724
69.9k
        }
725
69.7k
        if (indices.size() < flattenOps.size()) {
726
546
            std::vector<CoordinateOperationNNPtr> flattenOpsNew;
727
1.51k
            for (size_t i = 0; i < indices.size(); ++i) {
728
969
                flattenOpsNew.emplace_back(flattenOps[indices[i]]);
729
969
            }
730
546
            flattenOps = std::move(flattenOpsNew);
731
546
        }
732
69.7k
    }
733
734
119k
    if (flattenOps.size() == 1) {
735
368
        return flattenOps[0];
736
368
    }
737
738
119k
    properties.set(common::IdentifiedObject::NAME_KEY,
739
119k
                   computeConcatenatedName(flattenOps));
740
741
119k
    bool emptyIntersection = false;
742
119k
    auto extent = getExtent(flattenOps, false, emptyIntersection);
743
119k
    if (checkExtent && emptyIntersection) {
744
10.5k
        std::string msg(
745
10.5k
            "empty intersection of area of validity of concatenated "
746
10.5k
            "operations");
747
10.5k
        throw InvalidOperationEmptyIntersection(msg);
748
10.5k
    }
749
108k
    if (extent) {
750
107k
        properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY,
751
107k
                       NN_NO_CHECK(extent));
752
107k
    }
753
754
108k
    std::vector<metadata::PositionalAccuracyNNPtr> accuracies;
755
108k
    const double accuracy = getAccuracy(flattenOps);
756
108k
    if (accuracy >= 0.0) {
757
48.9k
        accuracies.emplace_back(
758
48.9k
            metadata::PositionalAccuracy::create(toString(accuracy)));
759
48.9k
    }
760
761
108k
    auto op = create(properties, flattenOps, accuracies);
762
108k
    op->setHasBallparkTransformation(hasBallparkTransformation);
763
108k
    op->d->computedName_ = true;
764
108k
    return op;
765
119k
}
766
767
// ---------------------------------------------------------------------------
768
769
59.7k
CoordinateOperationNNPtr ConcatenatedOperation::inverse() const {
770
59.7k
    std::vector<CoordinateOperationNNPtr> inversedOperations;
771
59.7k
    auto l_operations = operations();
772
59.7k
    inversedOperations.reserve(l_operations.size());
773
173k
    for (const auto &operation : l_operations) {
774
173k
        inversedOperations.emplace_back(operation->inverse());
775
173k
    }
776
59.7k
    std::reverse(inversedOperations.begin(), inversedOperations.end());
777
778
59.7k
    auto properties = createPropertiesForInverse(this, false, false);
779
59.7k
    if (d->computedName_) {
780
43.5k
        properties.set(common::IdentifiedObject::NAME_KEY,
781
43.5k
                       computeConcatenatedName(inversedOperations));
782
43.5k
    }
783
784
59.7k
    auto op =
785
59.7k
        create(properties, inversedOperations, coordinateOperationAccuracies());
786
59.7k
    op->d->computedName_ = d->computedName_;
787
59.7k
    op->setHasBallparkTransformation(hasBallparkTransformation());
788
59.7k
    op->setSourceCoordinateEpoch(targetCoordinateEpoch());
789
59.7k
    op->setTargetCoordinateEpoch(sourceCoordinateEpoch());
790
59.7k
    return op;
791
59.7k
}
792
793
// ---------------------------------------------------------------------------
794
795
//! @cond Doxygen_Suppress
796
0
void ConcatenatedOperation::_exportToWKT(io::WKTFormatter *formatter) const {
797
0
    const bool isWKT2 = formatter->version() == io::WKTFormatter::Version::WKT2;
798
0
    if (!isWKT2 || !formatter->use2019Keywords()) {
799
0
        throw io::FormattingException(
800
0
            "Transformation can only be exported to WKT2:2019");
801
0
    }
802
803
0
    formatter->startNode(io::WKTConstants::CONCATENATEDOPERATION,
804
0
                         !identifiers().empty());
805
0
    formatter->addQuotedString(nameStr());
806
807
0
    if (formatter->use2019Keywords()) {
808
0
        const auto &version = operationVersion();
809
0
        if (version.has_value()) {
810
0
            formatter->startNode(io::WKTConstants::VERSION, false);
811
0
            formatter->addQuotedString(*version);
812
0
            formatter->endNode();
813
0
        }
814
0
    }
815
816
0
    exportSourceCRSAndTargetCRSToWKT(this, formatter);
817
818
0
    const bool canExportOperationId =
819
0
        !(formatter->idOnTopLevelOnly() && formatter->topLevelHasId());
820
821
0
    const bool hasDomains = !domains().empty();
822
0
    if (hasDomains) {
823
0
        formatter->pushDisableUsage();
824
0
    }
825
826
0
    for (const auto &operation : operations()) {
827
0
        formatter->startNode(io::WKTConstants::STEP, false);
828
0
        if (canExportOperationId && !operation->identifiers().empty()) {
829
            // fake that top node has no id, so that the operation id is
830
            // considered
831
0
            formatter->pushHasId(false);
832
0
            operation->_exportToWKT(formatter);
833
0
            formatter->popHasId();
834
0
        } else {
835
0
            operation->_exportToWKT(formatter);
836
0
        }
837
0
        formatter->endNode();
838
0
    }
839
840
0
    if (hasDomains) {
841
0
        formatter->popDisableUsage();
842
0
    }
843
844
0
    if (!coordinateOperationAccuracies().empty()) {
845
0
        formatter->startNode(io::WKTConstants::OPERATIONACCURACY, false);
846
0
        formatter->add(coordinateOperationAccuracies()[0]->value());
847
0
        formatter->endNode();
848
0
    }
849
850
0
    ObjectUsage::baseExportToWKT(formatter);
851
0
    formatter->endNode();
852
0
}
853
//! @endcond
854
855
// ---------------------------------------------------------------------------
856
857
//! @cond Doxygen_Suppress
858
void ConcatenatedOperation::_exportToJSON(
859
    io::JSONFormatter *formatter) const // throw(FormattingException)
860
0
{
861
0
    auto writer = formatter->writer();
862
0
    auto objectContext(formatter->MakeObjectContext("ConcatenatedOperation",
863
0
                                                    !identifiers().empty()));
864
865
0
    writer->AddObjKey("name");
866
0
    const auto &l_name = nameStr();
867
0
    if (l_name.empty()) {
868
0
        writer->Add("unnamed");
869
0
    } else {
870
0
        writer->Add(l_name);
871
0
    }
872
873
0
    writer->AddObjKey("source_crs");
874
0
    formatter->setAllowIDInImmediateChild();
875
0
    sourceCRS()->_exportToJSON(formatter);
876
877
0
    writer->AddObjKey("target_crs");
878
0
    formatter->setAllowIDInImmediateChild();
879
0
    targetCRS()->_exportToJSON(formatter);
880
881
0
    writer->AddObjKey("steps");
882
0
    {
883
0
        auto parametersContext(writer->MakeArrayContext(false));
884
0
        for (const auto &operation : operations()) {
885
0
            formatter->setAllowIDInImmediateChild();
886
0
            operation->_exportToJSON(formatter);
887
0
        }
888
0
    }
889
890
0
    if (!coordinateOperationAccuracies().empty()) {
891
0
        writer->AddObjKey("accuracy");
892
0
        writer->Add(coordinateOperationAccuracies()[0]->value());
893
0
    }
894
895
0
    ObjectUsage::baseExportToJSON(formatter);
896
0
}
897
898
//! @endcond
899
900
// ---------------------------------------------------------------------------
901
902
//! @cond Doxygen_Suppress
903
13.2k
CoordinateOperationNNPtr ConcatenatedOperation::_shallowClone() const {
904
13.2k
    auto op =
905
13.2k
        ConcatenatedOperation::nn_make_shared<ConcatenatedOperation>(*this);
906
13.2k
    std::vector<CoordinateOperationNNPtr> ops;
907
32.4k
    for (const auto &subOp : d->operations_) {
908
32.4k
        ops.emplace_back(subOp->shallowClone());
909
32.4k
    }
910
13.2k
    op->d->operations_ = std::move(ops);
911
13.2k
    op->assignSelf(op);
912
13.2k
    op->setCRSs(this, false);
913
13.2k
    return util::nn_static_pointer_cast<CoordinateOperation>(op);
914
13.2k
}
915
//! @endcond
916
917
// ---------------------------------------------------------------------------
918
919
void ConcatenatedOperation::_exportToPROJString(
920
    io::PROJStringFormatter *formatter) const // throw(FormattingException)
921
234k
{
922
234k
    double sourceYear =
923
234k
        sourceCoordinateEpoch().has_value()
924
234k
            ? getRoundedEpochInDecimalYear(
925
41
                  sourceCoordinateEpoch()->coordinateEpoch().convertToUnit(
926
41
                      common::UnitOfMeasure::YEAR))
927
234k
            : 0;
928
234k
    double targetYear =
929
234k
        targetCoordinateEpoch().has_value()
930
234k
            ? getRoundedEpochInDecimalYear(
931
36
                  targetCoordinateEpoch()->coordinateEpoch().convertToUnit(
932
36
                      common::UnitOfMeasure::YEAR))
933
234k
            : 0;
934
234k
    if (sourceYear > 0 && targetYear == 0)
935
40
        targetYear = sourceYear;
936
234k
    else if (targetYear > 0 && sourceYear == 0)
937
0
        sourceYear = targetYear;
938
234k
    if (sourceYear > 0) {
939
40
        formatter->addStep("set");
940
40
        formatter->addParam("v_4", sourceYear);
941
40
    }
942
701k
    for (const auto &operation : operations()) {
943
701k
        operation->_exportToPROJString(formatter);
944
701k
    }
945
234k
    if (targetYear > 0) {
946
39
        formatter->addStep("set");
947
39
        formatter->addParam("v_4", targetYear);
948
39
    }
949
234k
}
950
951
// ---------------------------------------------------------------------------
952
953
//! @cond Doxygen_Suppress
954
bool ConcatenatedOperation::_isEquivalentTo(
955
    const util::IComparable *other, util::IComparable::Criterion criterion,
956
35.1k
    const io::DatabaseContextPtr &dbContext) const {
957
35.1k
    auto otherCO = dynamic_cast<const ConcatenatedOperation *>(other);
958
35.1k
    if (otherCO == nullptr ||
959
33.5k
        (criterion == util::IComparable::Criterion::STRICT &&
960
27.7k
         !ObjectUsage::_isEquivalentTo(other, criterion, dbContext))) {
961
27.7k
        return false;
962
27.7k
    }
963
7.41k
    const auto &steps = operations();
964
7.41k
    const auto &otherSteps = otherCO->operations();
965
7.41k
    if (steps.size() != otherSteps.size()) {
966
3.32k
        return false;
967
3.32k
    }
968
9.82k
    for (size_t i = 0; i < steps.size(); i++) {
969
9.00k
        if (!steps[i]->_isEquivalentTo(otherSteps[i].get(), criterion,
970
9.00k
                                       dbContext)) {
971
3.27k
            return false;
972
3.27k
        }
973
9.00k
    }
974
818
    return true;
975
4.09k
}
976
//! @endcond
977
978
// ---------------------------------------------------------------------------
979
980
std::set<GridDescription> ConcatenatedOperation::gridsNeeded(
981
    const io::DatabaseContextPtr &databaseContext,
982
70.8k
    bool considerKnownGridsAsAvailable) const {
983
70.8k
    std::set<GridDescription> res;
984
223k
    for (const auto &operation : operations()) {
985
223k
        const auto l_gridsNeeded = operation->gridsNeeded(
986
223k
            databaseContext, considerKnownGridsAsAvailable);
987
223k
        for (const auto &gridDesc : l_gridsNeeded) {
988
130k
            res.insert(gridDesc);
989
130k
        }
990
223k
    }
991
70.8k
    return res;
992
70.8k
}
993
994
// ---------------------------------------------------------------------------
995
996
} // namespace operation
997
NS_PROJ_END