Coverage Report

Created: 2025-06-13 06:29

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