Coverage Report

Created: 2026-02-26 07:07

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
121k
        : operations_(operationsIn) {}
81
7.97k
    Private(const Private &) = default;
82
};
83
//! @endcond
84
85
// ---------------------------------------------------------------------------
86
87
//! @cond Doxygen_Suppress
88
129k
ConcatenatedOperation::~ConcatenatedOperation() = default;
89
//! @endcond
90
91
// ---------------------------------------------------------------------------
92
93
//! @cond Doxygen_Suppress
94
ConcatenatedOperation::ConcatenatedOperation(const ConcatenatedOperation &other)
95
7.97k
    : CoordinateOperation(other), d(std::make_unique<Private>(*(other.d))) {}
96
//! @endcond
97
98
// ---------------------------------------------------------------------------
99
100
ConcatenatedOperation::ConcatenatedOperation(
101
    const std::vector<CoordinateOperationNNPtr> &operationsIn)
102
121k
    : CoordinateOperation(), d(std::make_unique<Private>(operationsIn)) {
103
360k
    for (const auto &op : operationsIn) {
104
360k
        if (op->requiresPerCoordinateInputTime()) {
105
20.2k
            setRequiresPerCoordinateInputTime(true);
106
20.2k
            break;
107
20.2k
        }
108
360k
    }
109
121k
}
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
329k
ConcatenatedOperation::operations() const {
119
329k
    return d->operations_;
120
329k
}
121
122
// ---------------------------------------------------------------------------
123
124
//! @cond Doxygen_Suppress
125
407k
static bool areCRSMoreOrLessEquivalent(const crs::CRS *a, const crs::CRS *b) {
126
407k
    const auto &aIds = a->identifiers();
127
407k
    const auto &bIds = b->identifiers();
128
407k
    if (aIds.size() == 1 && bIds.size() == 1 &&
129
321k
        aIds[0]->code() == bIds[0]->code() &&
130
313k
        *aIds[0]->codeSpace() == *bIds[0]->codeSpace()) {
131
313k
        return true;
132
313k
    }
133
94.6k
    if (a->_isEquivalentTo(b, util::IComparable::Criterion::EQUIVALENT)) {
134
85.8k
        return true;
135
85.8k
    }
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
8.81k
    const auto compoundA = dynamic_cast<const crs::CompoundCRS *>(a);
142
8.81k
    const auto compoundB = dynamic_cast<const crs::CompoundCRS *>(b);
143
8.81k
    if (compoundA && !compoundB)
144
0
        return areCRSMoreOrLessEquivalent(
145
0
            compoundA->componentReferenceSystems()[1].get(), b);
146
8.81k
    else if (!compoundA && compoundB)
147
2
        return areCRSMoreOrLessEquivalent(
148
2
            a, compoundB->componentReferenceSystems()[1].get());
149
8.81k
    return false;
150
8.81k
}
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
121k
{
171
121k
    if (operationsIn.size() < 2) {
172
0
        throw InvalidOperation(
173
0
            "ConcatenatedOperation must have at least 2 operations");
174
0
    }
175
121k
    crs::CRSPtr lastTargetCRS;
176
177
121k
    crs::CRSPtr interpolationCRS;
178
121k
    bool interpolationCRSValid = true;
179
121k
    bool hasBallparkTransformation = false;
180
518k
    for (size_t i = 0; i < operationsIn.size(); i++) {
181
396k
        auto l_sourceCRS = operationsIn[i]->sourceCRS();
182
396k
        auto l_targetCRS = operationsIn[i]->targetCRS();
183
184
396k
        hasBallparkTransformation |=
185
396k
            operationsIn[i]->hasBallparkTransformation();
186
396k
        if (interpolationCRSValid) {
187
395k
            auto subOpInterpCRS = operationsIn[i]->interpolationCRS();
188
395k
            if (interpolationCRS == nullptr)
189
393k
                interpolationCRS = std::move(subOpInterpCRS);
190
1.53k
            else if (subOpInterpCRS == nullptr ||
191
1.52k
                     !(subOpInterpCRS->isEquivalentTo(
192
1.52k
                         interpolationCRS.get(),
193
1.53k
                         util::IComparable::Criterion::EQUIVALENT))) {
194
1.53k
                interpolationCRS = nullptr;
195
1.53k
                interpolationCRSValid = false;
196
1.53k
            }
197
395k
        }
198
199
396k
        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
396k
        if (i >= 1) {
204
274k
            if (!areCRSMoreOrLessEquivalent(l_sourceCRS.get(),
205
274k
                                            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
274k
        }
230
396k
        lastTargetCRS = std::move(l_targetCRS);
231
396k
    }
232
233
    // When chaining VerticalCRS -> GeographicCRS -> VerticalCRS, use
234
    // GeographicCRS as the interpolationCRS
235
121k
    const auto l_sourceCRS = NN_NO_CHECK(operationsIn[0]->sourceCRS());
236
121k
    const auto l_targetCRS = NN_NO_CHECK(operationsIn.back()->targetCRS());
237
121k
    if (operationsIn.size() == 2 && interpolationCRS == nullptr &&
238
59.7k
        dynamic_cast<const crs::VerticalCRS *>(l_sourceCRS.get()) != nullptr &&
239
400
        dynamic_cast<const crs::VerticalCRS *>(l_targetCRS.get()) != nullptr) {
240
166
        const auto geog1 = dynamic_cast<crs::GeographicCRS *>(
241
166
            operationsIn[0]->targetCRS().get());
242
166
        const auto geog2 = dynamic_cast<crs::GeographicCRS *>(
243
166
            operationsIn[1]->sourceCRS().get());
244
166
        if (geog1 != nullptr && geog2 != nullptr &&
245
137
            geog1->_isEquivalentTo(geog2,
246
137
                                   util::IComparable::Criterion::EQUIVALENT)) {
247
137
            interpolationCRS = operationsIn[0]->targetCRS();
248
137
        }
249
166
    }
250
251
121k
    auto op = ConcatenatedOperation::nn_make_shared<ConcatenatedOperation>(
252
121k
        operationsIn);
253
121k
    op->assignSelf(op);
254
121k
    op->setProperties(properties);
255
121k
    op->setHasBallparkTransformation(hasBallparkTransformation);
256
121k
    op->setCRSs(l_sourceCRS, l_targetCRS, interpolationCRS);
257
121k
    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
121k
    return op;
267
121k
}
268
269
// ---------------------------------------------------------------------------
270
271
//! @cond Doxygen_Suppress
272
273
// ---------------------------------------------------------------------------
274
275
/* static */ void
276
ConcatenatedOperation::setCRSsUpdateInverse(CoordinateOperation *co,
277
                                            const crs::CRSNNPtr &sourceCRS,
278
47
                                            const crs::CRSNNPtr &targetCRS) {
279
280
47
    co->setCRSsUpdateInverse(sourceCRS, targetCRS, co->interpolationCRS());
281
47
}
282
283
// ---------------------------------------------------------------------------
284
285
void ConcatenatedOperation::fixSteps(
286
    const crs::CRSNNPtr &concatOpSourceCRS,
287
    const crs::CRSNNPtr &concatOpTargetCRS,
288
    std::vector<CoordinateOperationNNPtr> &operationsInOut,
289
20.0k
    const io::DatabaseContextPtr & /*dbContext*/, bool fixDirectionAllowed) {
290
291
    // Set of heuristics to assign CRS to steps, and possibly reverse them.
292
293
20.0k
    const auto isGeographic = [](const crs::CRS *crs) -> bool {
294
4.82k
        return dynamic_cast<const crs::GeographicCRS *>(crs) != nullptr;
295
4.82k
    };
296
297
20.0k
    const auto isGeocentric = [](const crs::CRS *crs) -> bool {
298
3.16k
        const auto geodCRS = dynamic_cast<const crs::GeodeticCRS *>(crs);
299
3.16k
        return (geodCRS && geodCRS->isGeocentric());
300
3.16k
    };
301
302
    // Apply axis order reversal operation on first operation if needed
303
    // to set CRSs on it
304
20.0k
    if (operationsInOut.size() >= 1) {
305
20.0k
        auto &op = operationsInOut.front();
306
20.0k
        auto l_sourceCRS = op->sourceCRS();
307
20.0k
        auto l_targetCRS = op->targetCRS();
308
20.0k
        auto conv = dynamic_cast<const Conversion *>(op.get());
309
20.0k
        if (conv && !l_sourceCRS && !l_targetCRS &&
310
31
            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
20.0k
    }
316
317
    // Apply axis order reversal operation on last operation if needed
318
    // to set CRSs on it
319
20.0k
    if (operationsInOut.size() >= 2) {
320
20.0k
        auto &op = operationsInOut.back();
321
20.0k
        auto l_sourceCRS = op->sourceCRS();
322
20.0k
        auto l_targetCRS = op->targetCRS();
323
20.0k
        auto conv = dynamic_cast<const Conversion *>(op.get());
324
20.0k
        if (conv && !l_sourceCRS && !l_targetCRS &&
325
12
            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
20.0k
    }
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
20.0k
    if (fixDirectionAllowed && operationsInOut.size() >= 2) {
335
18.4k
        auto &op = operationsInOut.front();
336
18.4k
        auto l_sourceCRS = op->sourceCRS();
337
18.4k
        auto l_targetCRS = op->targetCRS();
338
18.4k
        if (l_sourceCRS && l_targetCRS &&
339
18.4k
            !areCRSMoreOrLessEquivalent(l_sourceCRS.get(),
340
18.4k
                                        concatOpSourceCRS.get()) &&
341
403
            areCRSMoreOrLessEquivalent(l_targetCRS.get(),
342
403
                                       concatOpSourceCRS.get())) {
343
18
            op = op->inverse();
344
18
        }
345
18.4k
    }
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
20.0k
    if (fixDirectionAllowed && operationsInOut.size() >= 2) {
350
18.4k
        auto &op = operationsInOut.back();
351
18.4k
        auto l_sourceCRS = op->sourceCRS();
352
18.4k
        auto l_targetCRS = op->targetCRS();
353
18.4k
        if (l_sourceCRS && l_targetCRS &&
354
18.4k
            !areCRSMoreOrLessEquivalent(l_targetCRS.get(),
355
18.4k
                                        concatOpTargetCRS.get()) &&
356
411
            areCRSMoreOrLessEquivalent(l_sourceCRS.get(),
357
411
                                       concatOpTargetCRS.get())) {
358
411
            op = op->inverse();
359
411
        }
360
18.4k
    }
361
362
20.0k
    const auto extractDerivedCRS =
363
20.0k
        [](const crs::CRS *crs) -> const crs::DerivedCRS * {
364
55
        auto derivedCRS = dynamic_cast<const crs::DerivedCRS *>(crs);
365
55
        if (derivedCRS)
366
9
            return derivedCRS;
367
46
        auto compoundCRS = dynamic_cast<const crs::CompoundCRS *>(crs);
368
46
        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
46
        return nullptr;
375
46
    };
376
377
70.7k
    for (size_t i = 0; i < operationsInOut.size(); ++i) {
378
50.6k
        auto &op = operationsInOut[i];
379
50.6k
        auto l_sourceCRS = op->sourceCRS();
380
50.6k
        auto l_targetCRS = op->targetCRS();
381
50.6k
        auto conv = dynamic_cast<const Conversion *>(op.get());
382
50.6k
        if (conv && i == 0 && !l_sourceCRS && !l_targetCRS) {
383
31
            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
31
            } else if (i + 1 < operationsInOut.size()) {
413
                /* coverity[copy_paste_error] */
414
31
                l_targetCRS = operationsInOut[i + 1]->sourceCRS();
415
31
                if (l_targetCRS) {
416
31
                    setCRSsUpdateInverse(op.get(), concatOpSourceCRS,
417
31
                                         NN_NO_CHECK(l_targetCRS));
418
31
                }
419
31
            }
420
50.6k
        } else if (conv && i + 1 == operationsInOut.size() && !l_sourceCRS &&
421
12
                   !l_targetCRS) {
422
12
            auto derivedCRS = extractDerivedCRS(concatOpTargetCRS.get());
423
12
            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
12
            } else if (i >= 1) {
448
12
                l_sourceCRS = operationsInOut[i - 1]->targetCRS();
449
12
                if (l_sourceCRS) {
450
12
                    derivedCRS = extractDerivedCRS(l_sourceCRS.get());
451
12
                    if (fixDirectionAllowed && derivedCRS &&
452
2
                        conv->isEquivalentTo(
453
2
                            derivedCRS->derivingConversion().get(),
454
2
                            util::IComparable::Criterion::EQUIVALENT)) {
455
2
                        op = op->inverse();
456
2
                    }
457
12
                    setCRSsUpdateInverse(op.get(), NN_NO_CHECK(l_sourceCRS),
458
12
                                         concatOpTargetCRS);
459
12
                }
460
12
            }
461
50.6k
        } 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
50.6k
        } 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
50.6k
            auto prevOpTarget = (i == 0) ? concatOpSourceCRS.as_nullable()
550
50.6k
                                         : operationsInOut[i - 1]->targetCRS();
551
50.6k
            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
50.6k
            if (areCRSMoreOrLessEquivalent(l_sourceCRS.get(),
557
50.6k
                                           prevOpTarget.get())) {
558
                // do nothing
559
46.1k
            } else if (fixDirectionAllowed &&
560
4.44k
                       areCRSMoreOrLessEquivalent(l_targetCRS.get(),
561
4.44k
                                                  prevOpTarget.get())) {
562
1.28k
                op = op->inverse();
563
1.28k
            }
564
            // Below is needed for EPSG:9103 which chains NAD83(2011) geographic
565
            // 2D with NAD83(2011) geocentric
566
3.16k
            else if (l_sourceCRS->nameStr() == prevOpTarget->nameStr() &&
567
1.89k
                     ((isGeographic(l_sourceCRS.get()) &&
568
1.50k
                       isGeocentric(prevOpTarget.get())) ||
569
385
                      (isGeocentric(l_sourceCRS.get()) &&
570
1.89k
                       isGeographic(prevOpTarget.get())))) {
571
1.89k
                auto newOp(Conversion::createGeographicGeocentric(
572
1.89k
                    NN_NO_CHECK(prevOpTarget), NN_NO_CHECK(l_sourceCRS)));
573
1.89k
                operationsInOut.insert(operationsInOut.begin() + i, newOp);
574
1.89k
            } else if (l_targetCRS->nameStr() == prevOpTarget->nameStr() &&
575
1.27k
                       ((isGeographic(l_targetCRS.get()) &&
576
0
                         isGeocentric(prevOpTarget.get())) ||
577
1.27k
                        (isGeocentric(l_targetCRS.get()) &&
578
1.27k
                         isGeographic(prevOpTarget.get())))) {
579
1.27k
                auto newOp(Conversion::createGeographicGeocentric(
580
1.27k
                    NN_NO_CHECK(prevOpTarget), NN_NO_CHECK(l_targetCRS)));
581
1.27k
                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
1.27k
            } 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
50.6k
        }
625
50.6k
    }
626
627
20.0k
    if (!operationsInOut.empty()) {
628
20.0k
        auto l_sourceCRS = operationsInOut.front()->sourceCRS();
629
20.0k
        if (l_sourceCRS && !areCRSMoreOrLessEquivalent(
630
20.0k
                               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
20.0k
        auto l_targetCRS = operationsInOut.back()->targetCRS();
638
20.0k
        if (l_targetCRS && !areCRSMoreOrLessEquivalent(
639
20.0k
                               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
20.0k
    }
656
20.0k
}
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
110k
{
678
110k
    util::PropertyMap properties;
679
680
110k
    if (operationsIn.size() == 1) {
681
46.5k
        return operationsIn[0];
682
46.5k
    }
683
684
63.6k
    std::vector<CoordinateOperationNNPtr> flattenOps;
685
63.6k
    bool hasBallparkTransformation = false;
686
150k
    for (const auto &subOp : operationsIn) {
687
150k
        hasBallparkTransformation |= subOp->hasBallparkTransformation();
688
150k
        auto subOpConcat =
689
150k
            dynamic_cast<const ConcatenatedOperation *>(subOp.get());
690
150k
        if (subOpConcat) {
691
33.4k
            auto subOps = subOpConcat->operations();
692
114k
            for (const auto &subSubOp : subOps) {
693
114k
                flattenOps.emplace_back(subSubOp);
694
114k
            }
695
116k
        } else {
696
116k
            flattenOps.emplace_back(subOp);
697
116k
        }
698
150k
    }
699
700
    // Remove consecutive inverse operations
701
63.6k
    if (flattenOps.size() > 2) {
702
37.4k
        std::vector<size_t> indices;
703
216k
        for (size_t i = 0; i < flattenOps.size(); ++i)
704
178k
            indices.push_back(i);
705
38.4k
        while (true) {
706
38.4k
            bool bHasChanged = false;
707
179k
            for (size_t i = 0; i + 1 < indices.size(); ++i) {
708
142k
                if (flattenOps[indices[i]]->_isEquivalentTo(
709
142k
                        flattenOps[indices[i + 1]]->inverse().get(),
710
142k
                        util::IComparable::Criterion::EQUIVALENT) &&
711
2.15k
                    flattenOps[indices[i]]->sourceCRS()->_isEquivalentTo(
712
2.15k
                        flattenOps[indices[i + 1]]->targetCRS().get(),
713
2.15k
                        util::IComparable::Criterion::EQUIVALENT)) {
714
1.52k
                    indices.erase(indices.begin() + i, indices.begin() + i + 2);
715
1.52k
                    bHasChanged = true;
716
1.52k
                    break;
717
1.52k
                }
718
142k
            }
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
38.4k
            if (!bHasChanged || indices.size() <= 2)
723
37.4k
                break;
724
38.4k
        }
725
37.4k
        if (indices.size() < flattenOps.size()) {
726
1.50k
            std::vector<CoordinateOperationNNPtr> flattenOpsNew;
727
7.59k
            for (size_t i = 0; i < indices.size(); ++i) {
728
6.09k
                flattenOpsNew.emplace_back(flattenOps[indices[i]]);
729
6.09k
            }
730
1.50k
            flattenOps = std::move(flattenOpsNew);
731
1.50k
        }
732
37.4k
    }
733
734
63.6k
    if (flattenOps.size() == 1) {
735
478
        return flattenOps[0];
736
478
    }
737
738
63.1k
    properties.set(common::IdentifiedObject::NAME_KEY,
739
63.1k
                   computeConcatenatedName(flattenOps));
740
741
63.1k
    bool emptyIntersection = false;
742
63.1k
    auto extent = getExtent(flattenOps, false, emptyIntersection);
743
63.1k
    if (checkExtent && emptyIntersection) {
744
16
        std::string msg(
745
16
            "empty intersection of area of validity of concatenated "
746
16
            "operations");
747
16
        throw InvalidOperationEmptyIntersection(msg);
748
16
    }
749
63.1k
    if (extent) {
750
62.5k
        properties.set(common::ObjectUsage::DOMAIN_OF_VALIDITY_KEY,
751
62.5k
                       NN_NO_CHECK(extent));
752
62.5k
    }
753
754
63.1k
    std::vector<metadata::PositionalAccuracyNNPtr> accuracies;
755
63.1k
    const double accuracy = getAccuracy(flattenOps);
756
63.1k
    if (accuracy >= 0.0) {
757
31.8k
        accuracies.emplace_back(
758
31.8k
            metadata::PositionalAccuracy::create(toString(accuracy)));
759
31.8k
    }
760
761
63.1k
    auto op = create(properties, flattenOps, accuracies);
762
63.1k
    op->setHasBallparkTransformation(hasBallparkTransformation);
763
63.1k
    op->d->computedName_ = true;
764
63.1k
    return op;
765
63.1k
}
766
767
// ---------------------------------------------------------------------------
768
769
38.7k
CoordinateOperationNNPtr ConcatenatedOperation::inverse() const {
770
38.7k
    std::vector<CoordinateOperationNNPtr> inversedOperations;
771
38.7k
    auto l_operations = operations();
772
38.7k
    inversedOperations.reserve(l_operations.size());
773
118k
    for (const auto &operation : l_operations) {
774
118k
        inversedOperations.emplace_back(operation->inverse());
775
118k
    }
776
38.7k
    std::reverse(inversedOperations.begin(), inversedOperations.end());
777
778
38.7k
    auto properties = createPropertiesForInverse(this, false, false);
779
38.7k
    if (d->computedName_) {
780
26.9k
        properties.set(common::IdentifiedObject::NAME_KEY,
781
26.9k
                       computeConcatenatedName(inversedOperations));
782
26.9k
    }
783
784
38.7k
    auto op =
785
38.7k
        create(properties, inversedOperations, coordinateOperationAccuracies());
786
38.7k
    op->d->computedName_ = d->computedName_;
787
38.7k
    op->setHasBallparkTransformation(hasBallparkTransformation());
788
38.7k
    op->setSourceCoordinateEpoch(targetCoordinateEpoch());
789
38.7k
    op->setTargetCoordinateEpoch(sourceCoordinateEpoch());
790
38.7k
    return op;
791
38.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
7.97k
CoordinateOperationNNPtr ConcatenatedOperation::_shallowClone() const {
904
7.97k
    auto op =
905
7.97k
        ConcatenatedOperation::nn_make_shared<ConcatenatedOperation>(*this);
906
7.97k
    std::vector<CoordinateOperationNNPtr> ops;
907
20.2k
    for (const auto &subOp : d->operations_) {
908
20.2k
        ops.emplace_back(subOp->shallowClone());
909
20.2k
    }
910
7.97k
    op->d->operations_ = std::move(ops);
911
7.97k
    op->assignSelf(op);
912
7.97k
    op->setCRSs(this, false);
913
7.97k
    return util::nn_static_pointer_cast<CoordinateOperation>(op);
914
7.97k
}
915
//! @endcond
916
917
// ---------------------------------------------------------------------------
918
919
void ConcatenatedOperation::_exportToPROJString(
920
    io::PROJStringFormatter *formatter) const // throw(FormattingException)
921
94.4k
{
922
94.4k
    double sourceYear =
923
94.4k
        sourceCoordinateEpoch().has_value()
924
94.4k
            ? getRoundedEpochInDecimalYear(
925
25
                  sourceCoordinateEpoch()->coordinateEpoch().convertToUnit(
926
25
                      common::UnitOfMeasure::YEAR))
927
94.4k
            : 0;
928
94.4k
    double targetYear =
929
94.4k
        targetCoordinateEpoch().has_value()
930
94.4k
            ? getRoundedEpochInDecimalYear(
931
10
                  targetCoordinateEpoch()->coordinateEpoch().convertToUnit(
932
10
                      common::UnitOfMeasure::YEAR))
933
94.4k
            : 0;
934
94.4k
    if (sourceYear > 0 && targetYear == 0)
935
25
        targetYear = sourceYear;
936
94.4k
    else if (targetYear > 0 && sourceYear == 0)
937
3
        sourceYear = targetYear;
938
94.4k
    if (sourceYear > 0) {
939
28
        formatter->addStep("set");
940
28
        formatter->addParam("v_4", sourceYear);
941
28
    }
942
307k
    for (const auto &operation : operations()) {
943
307k
        operation->_exportToPROJString(formatter);
944
307k
    }
945
94.4k
    if (targetYear > 0) {
946
26
        formatter->addStep("set");
947
26
        formatter->addParam("v_4", targetYear);
948
26
    }
949
94.4k
}
950
951
// ---------------------------------------------------------------------------
952
953
//! @cond Doxygen_Suppress
954
bool ConcatenatedOperation::_isEquivalentTo(
955
    const util::IComparable *other, util::IComparable::Criterion criterion,
956
18.6k
    const io::DatabaseContextPtr &dbContext) const {
957
18.6k
    auto otherCO = dynamic_cast<const ConcatenatedOperation *>(other);
958
18.6k
    if (otherCO == nullptr ||
959
16.4k
        (criterion == util::IComparable::Criterion::STRICT &&
960
13.9k
         !ObjectUsage::_isEquivalentTo(other, criterion, dbContext))) {
961
13.9k
        return false;
962
13.9k
    }
963
4.63k
    const auto &steps = operations();
964
4.63k
    const auto &otherSteps = otherCO->operations();
965
4.63k
    if (steps.size() != otherSteps.size()) {
966
2.84k
        return false;
967
2.84k
    }
968
3.25k
    for (size_t i = 0; i < steps.size(); i++) {
969
2.90k
        if (!steps[i]->_isEquivalentTo(otherSteps[i].get(), criterion,
970
2.90k
                                       dbContext)) {
971
1.43k
            return false;
972
1.43k
        }
973
2.90k
    }
974
353
    return true;
975
1.78k
}
976
//! @endcond
977
978
// ---------------------------------------------------------------------------
979
980
std::set<GridDescription> ConcatenatedOperation::gridsNeeded(
981
    const io::DatabaseContextPtr &databaseContext,
982
42.9k
    bool considerKnownGridsAsAvailable) const {
983
42.9k
    std::set<GridDescription> res;
984
163k
    for (const auto &operation : operations()) {
985
163k
        const auto l_gridsNeeded = operation->gridsNeeded(
986
163k
            databaseContext, considerKnownGridsAsAvailable);
987
163k
        for (const auto &gridDesc : l_gridsNeeded) {
988
72.9k
            res.insert(gridDesc);
989
72.9k
        }
990
163k
    }
991
42.9k
    return res;
992
42.9k
}
993
994
// ---------------------------------------------------------------------------
995
996
} // namespace operation
997
NS_PROJ_END