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