/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 |